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 535Resultado: 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#
- AUTH LOGIN: El cliente solicita usar el mecanismo AUTH LOGIN
- Usuario en base64: El servidor responde con 334, espera el usuario codificado
- Contraseña en base64: El servidor responde con 334, espera la contraseña codificada
- 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Ñ') # OKConsideraciones 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.