Ir al contenido

AUTH LOGIN manual en Python con smtplib: caracteres especiales y error 535

·3 mins
Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.

El problema
#

Hace unos días intenté automatizar el envío de correos desde mi servidor doméstico. Nada complicado: un script Python con smtplib para enviar notificaciones. El problema llegó cuando configuré una contraseña con caracteres especiales: MiPasw0rd$Ñ.

import smtplib

server = smtplib.SMTP('mail.example.com', 587)
server.starttls()
server.login('usuario@example.com', 'MiPasw0rd$Ñ')  # Error 535

Resultado: SMTPAuthenticationError: (535, b'5.7.8 Authentication credentials invalid')

Lo extraño es que la contraseña era correcta. Accedía manualmente sin problemas. El error 535 sugería fallos de autenticación, pero el problema real estaba en la codificación.

Por qué falla
#

La culpa es del manejo de encoding en smtplib. El método login() utiliza por defecto UTF-8, pero luego aplica transformaciones que no siempre respetan los caracteres especiales correctamente, especialmente cuando el servidor o la librería tienen configuraciones legadas.

En mi caso, el servidor SMTP esperaba una codificación UTF-8 consistente. Cuando smtplib procesaba la contraseña con $ y Ñ, algo en el camino se corrompía.

La solución: AUTH LOGIN manual
#

El protocolo AUTH LOGIN es simple: se codifican usuario y contraseña en base64 por separado y se envían en pasos manuales. Esto te da control total sobre la codificación.

import smtplib
import base64

def auth_login_manual(server, usuario, contraseña):
    """Implementa AUTH LOGIN manualmente con UTF-8"""
    
    # Respuesta inicial del servidor
    code, response = server.docmd("AUTH LOGIN")
    
    # Codificar usuario en base64 UTF-8
    usuario_b64 = base64.b64encode(usuario.encode('utf-8')).decode('ascii')
    code, response = server.docmd(usuario_b64)
    
    # Codificar contraseña en base64 UTF-8
    contraseña_b64 = base64.b64encode(contraseña.encode('utf-8')).decode('ascii')
    code, response = server.docmd(contraseña_b64)
    
    # Verificar éxito (código 235 es autenticación exitosa)
    if code != 235:
        raise Exception(f"Autenticación fallida: {code} {response}")
    
    return True

# Uso
server = smtplib.SMTP('mail.example.com', 587)
server.starttls()
auth_login_manual(server, 'usuario@example.com', 'MiPasw0rd$Ñ')
print("Autenticado exitosamente")

Desglose del protocolo
#

  1. AUTH LOGIN: El cliente solicita usar el mecanismo AUTH LOGIN
  2. Usuario en base64: El servidor responde con 334, espera el usuario codificado
  3. Contraseña en base64: El servidor responde con 334, espera la contraseña codificada
  4. Respuesta 235: Indica autenticación exitosa

El método docmd() envía comandos SMTP crudos y devuelve el código de respuesta y el mensaje.

Prueba en mi servidor
#

Implementé esto en mi infraestructura doméstica con Postfix. La diferencia es notable:

# Antes: falla con caracteres especiales
server.login('admin@local.home', 'P@ssw0rdÑ')  # Error 535

# Después: funciona perfectamente
auth_login_manual(server, 'admin@local.home', 'P@ssw0rdÑ')  # OK

Consideraciones de seguridad
#

  • Base64 no es cifrado: usa siempre STARTTLS o conexión directa a puerto 465 con SSL
  • El encoding UTF-8 es seguro para caracteres especiales
  • Este método es compatible con cualquier servidor SMTP que soporte AUTH LOGIN

Script completo
#

import smtplib
import base64

class SMTPConnection:
    def __init__(self, host, port=587):
        self.server = smtplib.SMTP(host, port)
        self.server.starttls()
    
    def login(self, usuario, contraseña):
        self.server.docmd("AUTH LOGIN")
        self.server.docmd(base64.b64encode(usuario.encode('utf-8')).decode('ascii'))
        code, resp = self.server.docmd(base64.b64encode(contraseña.encode('utf-8')).decode('ascii'))
        if code != 235:
            raise Exception(f"Auth failed: {code}")
    
    def send(self, de, para, asunto, cuerpo):
        msg = f"From: {de}\nTo: {para}\nSubject: {asunto}\n\n{cuerpo}"
        self.server.sendmail(de, para, msg)
    
    def close(self):
        self.server.quit()

# Uso
smtp = SMTPConnection('mail.example.com')
smtp.login('usuario@example.com', 'MiPasw0rd$Ñ')
smtp.send('usuario@example.com', 'destino@example.com', 'Test', 'Funciona!')
smtp.close()

Conclusión
#

Si tu servidor SMTP rechaza contraseñas con caracteres especiales, no es un misterio. Es un problema de codificación. Implementar AUTH LOGIN manualmente te da control total y funciona con cualquier contraseña.

Lo apliqué en producción en mi servidor doméstico y no he tenido problemas desde entonces.