guía · SMTP con Python 3
¿Cómo debería implementar un equipo de producto SMTP con Python 3 de forma segura?
Implemente SMTP con Python 3 detrás de un worker de servidor autorizado, no en código del navegador ni controlado por el usuario. Construya los mensajes con EmailMessage, mantenga los destinatarios del sobre separados de los encabezados visibles, cree un contexto SSL verificado, establezca tiempos de espera de conexión finitos y use SMTP_SSL para TLS desde el inicio de la conexión o SMTP.starttls() seguido de EHLO para una actualización explícita. Cargue las credenciales desde un gestor de secretos, llame a send_message(), revise los resultados de destinatarios rechazados y persista el resultado exacto del intento. Reintente solo los fallos transitorios con backoff acotado y nunca trate la aceptación SMTP como prueba de llegada a la bandeja de entrada.
Defina una única operación de correo autorizada
Parta de un evento de producto aprobado, como la verificación de una cuenta, un recibo, una alerta solicitada o una notificación de seguridad. Guarde un trabajo saliente duradero antes de abrir una conexión SMTP. Ese trabajo debería contener una clave estable del evento de negocio, el inquilino, la clase de mensaje, la revisión de la plantilla, el remitente y los destinatarios del sobre aprobados, la identidad From visible, la base de consentimiento o necesidad y el resultado de supresión vigente. Ni el navegador, ni la aplicación móvil, ni la plantilla, ni la entrada del usuario deben poder elegir hosts SMTP, credenciales, remitentes del sobre, encabezados arbitrarios o destinatarios sin restricciones. Autorice a quien llama y al inquilino, valide las direcciones, limite el número de destinatarios y de adjuntos e impida la inyección de saltos de línea. Reclame cada trabajo una sola vez y mantenga un historial de intentos de solo anexado. smtplib de Python es un cliente del protocolo; no ofrece idempotencia de negocio, aislamiento entre inquilinos, consentimiento, supresión ni una cola duradera. Esos controles corresponden a la aplicación que lo rodea.
Construya mensajes estructurados con EmailMessage
Use email.message.EmailMessage en lugar de concatenar cadenas de encabezados y MIME. Establezca From, To, Subject y un encabezado de correlación estable de la aplicación a partir de valores validados; luego llame a set_content para el texto sin formato, a add_alternative para una parte HTML cuando sea necesario y a add_attachment solo para tipos y tamaños de archivo admitidos explícitamente. Genere el texto y el HTML a partir de la misma revisión aprobada de la plantilla. Escape los valores no confiables según su contexto de salida y evite renderizar HTML del usuario sin procesar. No ponga secretos, tokens de acceso, datos personales innecesarios ni claves internas de la base de datos en encabezados, asuntos, campos de seguimiento o nombres de adjuntos. El paquete email serializa según su política y puede generar límites MIME durante el aplanamiento, así que firme o calcule el hash de la representación serializada final si los controles de integridad posteriores dependen de los bytes exactos. Mantenga el sobre SMTP aparte: los encabezados visibles To y Cc informan a los lectores, mientras que la lista de destinatarios del transporte controla los comandos RCPT TO.
Elija TLS implícito o STARTTLS de forma deliberada
Use SMTP_SSL cuando el servidor exija TLS desde el inicio de la conexión. Use SMTP para una conexión en texto claro solo cuando el flujo documentado del servidor exija una actualización STARTTLS inmediata. La documentación de smtplib de Python indica que starttls coloca los comandos SMTP posteriores dentro de TLS y que el cliente debe volver a llamar a ehlo después. Nunca se autentique antes de la actualización TLS requerida. Cree el contexto con ssl.create_default_context para que la validación de certificados y la comprobación del nombre de host usen valores predeterminados seguros para el cliente, y pase el nombre de host esperado del servidor mediante la conexión normal de la biblioteca. Trate la falta de soporte de STARTTLS, un fallo de certificado, una discrepancia en el nombre de host o un fallo de negociación TLS como una detención definitiva cuando el cifrado sea obligatorio. No desactive la verificación ni la sustituya por un contexto sin verificar para que producción funcione. El TLS por salto protege la conexión SMTP, no el contenido del mensaje almacenado, el procesamiento del proveedor, el almacenamiento del receptor ni el buzón final.
Mantenga las credenciales en el servidor y con un alcance limitado
Cargue el nombre de usuario, la contraseña o el token SMTP en tiempo de ejecución desde un servicio de secretos gestionado. Nunca los ponga en el control de versiones, en capas de Docker, en configuración confirmada en Git, en URL, en argumentos de línea de comandos, en la salida de depuración, en analíticas, en informes de excepciones, en snapshots de pruebas, en notebooks, en tickets ni en prompts. Prefiera una credencial limitada a un entorno, a un dominio remitente o a una carga de trabajo permitida antes que un secreto administrativo para toda la cuenta. Separe producción de desarrollo y de CI. Convierta la rotación en rutina: aprovisione un reemplazo, actualice el worker, haga una prueba de entrega controlada, confirme la evidencia de autenticación y de resultados, y luego revoque la credencial antigua. Limite el acceso al secreto al proceso de envío y audite las lecturas administrativas. El método login de Python prueba los mecanismos de autenticación que anuncia el servidor; la aplicación sigue teniendo que decidir si el servidor, la seguridad de la conexión, la cuenta y el mecanismo son aceptables. Los fallos de autenticación repetidos deberían pausar la cohorte y desencadenar una investigación, no reintentos rápidos de contraseña.
Use tiempos de espera explícitos y una vida de conexión acotada
Pase un timeout finito a SMTP o SMTP_SSL para que las operaciones de conexión y las operaciones bloqueantes no puedan ocupar un worker indefinidamente. Aplique un plazo externo al trabajo y una política de cancelación, porque un tiempo de espera de socket no es un control completo de la antigüedad en la cola. No mantenga un objeto SMTP compartido entre tareas concurrentes, salvo que el acceso esté serializado y se haya demostrado que su estado es seguro. Un diseño sencillo abre una conexión para un lote acotado, saluda al servidor, establece TLS si es necesario, se autentica, envía un número reducido de mensajes, llama a quit y descarta la conexión tras un error o al alcanzar un límite de antigüedad. La reutilización puede reducir la sobrecarga, pero aumenta la ambigüedad después de desconexiones del servidor, tiempos de espera agotados o estados parciales. Limite los mensajes por conexión y reconecte de forma deliberada. Monitoree la latencia de conexión, la negociación TLS, la autenticación, la latencia de los comandos, las desconexiones del servidor y la antigüedad de los trabajos, sin registrar credenciales ni contenido de mensajes. El servidor SMTP puede imponer límites que cambian con independencia de Python.
Envíe un mensaje y conserve los resultados de destinatarios parciales
SMTP.sendmail usa from_addr y to_addrs para el sobre de transporte y no reescribe los encabezados del mensaje. SMTP.send_message serializa un EmailMessage y deriva valores predeterminados, salvo que se proporcionen valores explícitos para el sobre. En código de producción, pase explícitamente el remitente del sobre y la lista de destinatarios aprobados para que el manejo de Bcc y la autorización del inquilino no sean ambiguos. Python documenta que sendmail retorna con normalidad cuando se aceptó al menos un destinatario y devuelve un diccionario con cada destinatario rechazado. Por tanto, que no se produzca ninguna excepción no equivale a que todos los destinatarios se hayan aceptado. Guarde por separado los conjuntos de destinatarios aceptados y rechazados, incluidos el código de estado y el diagnóstico depurado. No reintente los destinatarios aceptados cuando solo se rechazaron algunos. Trate cada destinatario como un resultado autorizado de forma independiente, conservando el intento de mensaje compartido. Una excepción posterior en la etapa DATA es distinta de un rechazo en RCPT y necesita su propia clasificación.
Clasifique las excepciones por etapa y permanencia
Maneje explícitamente las excepciones de smtplib y conserve sus códigos SMTP y los mensajes del servidor depurados. SMTPConnectError y timeout pueden ser transitorios, pero también pueden revelar un host, un puerto o un firewall equivocados, o una caída. Un SMTPNotSupportedError tras STARTTLS o SMTPUTF8 debería detener una configuración que requiere esa función. SMTPAuthenticationError exige investigar las credenciales, la cuenta, el mecanismo y TLS, no reintentar a ciegas. SMTPSenderRefused y SMTPRecipientsRefused requieren decisiones con alcance de identidad o de destinatario. SMTPDataError describe una respuesta inesperada a DATA y puede representar un problema de contenido, de política, de cuota o un comportamiento temporal del receptor, según su estado extendido. Clasifique las respuestas 4xx como candidatas a un reintento acotado y las 5xx como permanentes para ese intento, respetando la documentación específica del proveedor. Use backoff exponencial, jitter, límites de intentos y de antigüedad en la cola, y un estado de dead-letter. Nunca reintente después de una supresión, una queja, una baja, una autorización revocada o evidencia de un destinatario no válido.
Concilie los resultados de envío ambiguos
Un tiempo de espera de red o una desconexión después de que el cliente transmitiera los datos del mensaje, pero antes de que observara la respuesta final del servidor, es ambiguo. El servidor podría haber asumido la responsabilidad aunque Python haya lanzado una excepción. No cree de inmediato un nuevo envío lógico. Marque el intento como desconocido, conserve sus identificadores estables de evento y de traza, y consulte los registros del proveedor o los eventos de entrega posteriores cuando estén disponibles. Si el servicio SMTP no ofrece idempotencia ni una correlación consultable, defina una decisión de producto basada en la clase de mensaje, la antigüedad, el daño de un duplicado y la experiencia del usuario. Las alertas de seguridad y los mensajes de restablecimiento de contraseña tienen riesgos de duplicado distintos a los de los recibos o los avisos financieros. Conserve en el registro el intento original y cualquier vínculo de reintento. Nunca afirme una entrega exactamente una vez, porque SMTP no la ofrece de extremo a extremo. Pruebe esta rama con un fixture de servidor controlado que corte la conexión en cada etapa del protocolo, incluso antes y después de la aceptación de DATA.
Separe la aceptación SMTP de la entrega y la interacción
Una llamada exitosa a send_message significa que al menos un destinatario fue aceptado en la etapa SMTP observada, según la semántica documentada de Python. No demuestra que se aceptara a todos los destinatarios, que el servidor de destino conservara después el mensaje, que el mensaje llegara a la carpeta de la bandeja de entrada ni que una persona lo leyera. Modele como evidencias separadas el envío al proveedor, la aceptación por el servidor del destinatario, los fallos temporales o permanentes, el rebote posterior, la queja, la baja, la ubicación en el buzón y la interacción. Incorpore los eventos autenticados del proveedor cuando estén disponibles, elimine duplicados y conserve la hora del suceso separada de la hora de procesamiento. Aplique los rebotes permanentes, las quejas y las bajas justo antes de los envíos posteriores. Las aperturas y los clics no son una prueba del transporte y pueden verse afectados por las tecnologías de privacidad. Mantenga métricas agregadas con datos personales minimizados por cohorte segura para los inquilinos, revisión de plantilla, dominio remitente, clase de estado y tiempo. Configure alertas para los picos de rechazos, los resultados desconocidos, la antigüedad de la cola, los fallos de TLS, los fallos de autenticación y una distribución inusual de destinatarios.
Haga pruebas locales sin enviar correo real a clientes
Realice pruebas unitarias de la construcción de mensajes, el rechazo de la inyección de encabezados, la autorización de destinatarios, la eliminación de Bcc, las alternativas de texto sin formato y HTML, el manejo de Unicode, los límites de adjuntos y las comprobaciones de supresión. Use un servidor de prueba SMTP local controlado o un fixture de protocolo para simular fallos de saludo, falta de STARTTLS, fallo de certificado, errores de autenticación, aceptación parcial de RCPT, respuestas DATA 4xx y 5xx, desconexiones y respuestas retrasadas. No use servicios de depuración sin autenticación obsoletos con secretos similares a los de producción o contenido de clientes. Las pruebas de integración deben usar cuentas dedicadas y destinatarios controlados, con cuotas y limpieza explícitas. Verifique el mensaje sin procesar recibido, los resultados de autenticación, los encabezados visibles, el comportamiento de respuesta y la correlación de eventos. Ejecute un análisis de secretos en los fixtures y los registros.
Cómo encaja SendHQ
SendHQ documenta una API de correo electrónico con alcance por espacio de trabajo para envío desde dominios verificados, eventos de entrega y supresiones. Esta guía cubre el cliente SMTP de la biblioteca estándar de Python; use la documentación de SendHQ para conocer sus métodos de integración y contrato de API actuales.
Preguntas frecuentes
¿Se deben poner las credenciales SMTP de Python en el código del cliente?
No. Manténgalas en un gestor de secretos del lado del servidor, con un alcance estrecho por entorno y carga de trabajo, acceso auditado, rotación periódica y sin registrarlas.
¿Cuándo se debe usar SMTP_SSL en Python?
Use SMTP_SSL cuando se requiera TLS desde el inicio de la conexión. Use SMTP con starttls solo para un flujo documentado de actualización explícita que falle de forma cerrada.
¿Se debe volver a llamar a EHLO después de starttls?
Sí. La documentación de smtplib de Python indica que se debe volver a llamar a ehlo después de starttls para que las capacidades se vuelvan a descubrir dentro de la conexión protegida.
¿send_message significa que se aceptó a todos los destinatarios?
No. Python puede retornar con normalidad cuando se aceptó al menos un destinatario y devuelve por separado los destinatarios rechazados. Persista y gestione los resultados de cada destinatario de forma independiente.
¿Qué debe ocurrir después de un SMTPAuthenticationError?
Pause la configuración afectada y revise TLS, el servidor, la cuenta, el secreto y los mecanismos anunciados. Reintentar credenciales a ciegas puede amplificar los bloqueos o las señales de compromiso.
¿Se debe reintentar todo SMTPDataError?
No. Conserve el estado y el diagnóstico exactos, y luego distinga las condiciones temporales 4xx de los fallos permanentes 5xx de política, contenido, cuota o configuración.
¿La aceptación SMTP determina la llegada a la bandeja de entrada?
No. Es evidencia acotada del transporte. Los relays posteriores, el filtrado del receptor, los rebotes, las reglas del buzón, la carpeta de destino y la interacción humana siguen siendo resultados independientes.
¿Esta guía cubre la integración específica de SendHQ?
No. Cubre el cliente SMTP de la biblioteca estándar de Python. Consulte la documentación de SendHQ sobre sus métodos de integración y contrato de API actuales.
Fuentes
- Documentación de smtplib de Python 3 — Python Software Foundation
- Documentación de EmailMessage de Python 3 — Python Software Foundation
- Documentación de ssl de Python 3 — Python Software Foundation
- RFC 5321: protocolo simple de transferencia de correo (SMTP) — RFC Editor
- RFC 3207: extensión del servicio SMTP para SMTP seguro sobre TLS — RFC Editor
- RFC 4954: extensión del servicio SMTP para la autenticación — RFC Editor