Enviar Emails con SMTP en Python, Node.js y Java
Aprende a enviar emails transaccionales y masivos vía SMTP con Python, Node.js y Java. Cubre autenticación, plantillas, adjuntos y entregabilidad.
Visión General
La mayoría de las aplicaciones aún dependen del email para resets de contraseña, confirmaciones de orden, newsletters y alertas. SMTP (Simple Mail Transfer Protocol) mueve esos mensajes desde tu aplicación hasta la bandeja del destinatario. Usarlo directamente te mantiene portable entre proveedores y te da control total sobre plantillas, adjuntos y reputación de remitente. Esta receta cubre patrones listos para producción en Python, Node.js y Java, con TLS, cuerpos de texto y HTML, adjuntos y limitación de tasa.
Cuándo Usar
- Tu aplicación envía emails transaccionales y quieres poder cambiar de proveedor sin reescribir código.
- Necesitas plantillas que traigan datos en vivo, o simplemente prefieres mantener la lógica de email dentro de tu código.
- Necesitas adjuntar facturas, reportes o exportaciones a los mensajes salientes y quieres control total sobre la construcción MIME.
- Ya operas una cola de mensajes o un pool de workers y quieres que remitentes SMTP extraigan trabajos de ella.
- Estás combinando email con buenas prácticas de rate limiting y gestión de secretos.
Cuándo Evitar
- Tu volumen es millones de emails de marketing al mes. Plataformas como Mailchimp, Brevo y Mailgun manejan la higiene de listas y el cumplimiento de bajas mucho mejor.
- Tu equipo no puede hacerse cargo de registros DNS (SPF, DKIM, DMARC), rebotes y reputación de remitente.
- Las notificaciones in-app necesitan baja latencia. Para esos casos, push o WebSockets suelen encajar mejor.
Solución
Python
import mimetypes
import smtplib
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pathlib import Path
from string import Template
SMTP_HOST = "smtp.example.com"
SMTP_PORT = 587
SMTP_USER = "user@example.com"
SMTP_PASS = "app-password"
def build_message(from_addr, to, subject, text, html=None, attachments=None):
"""Construye un email multipart con texto, HTML opcional y adjuntos."""
msg = MIMEMultipart("mixed")
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = to
alt = MIMEMultipart("alternative")
alt.attach(MIMEText(text, "plain"))
if html:
alt.attach(MIMEText(html, "html"))
msg.attach(alt)
if attachments:
for path in attachments:
ctype, _ = mimetypes.guess_type(path)
if ctype is None:
ctype = "application/octet-stream"
maintype, subtype = ctype.split("/", 1)
with open(path, "rb") as f:
part = MIMEBase(maintype, subtype)
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
"attachment",
filename=Path(path).name,
)
msg.attach(part)
return msg
def send_template_email(to, subject, template_text, template_html, context):
text = Template(template_text).substitute(context)
html = Template(template_html).substitute(context) if template_html else None
msg = build_message(SMTP_USER, to, subject, text, html)
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
server.send_message(msg)
# Uso
text_template = "Hola $name, tu orden $order_id está confirmada."
html_template = "<p>Hola $name, tu orden <b>$order_id</b> está confirmada.</p>"
send_template_email(
to="user@example.com",
subject="Orden #12345 confirmada",
template_text=text_template,
template_html=html_template,
context={"name": "Alice", "order_id": "12345"},
)
JavaScript (Node.js)
const fs = require('fs');
const nodemailer = require('nodemailer');
const path = require('path');
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
secure: false,
auth: { user: 'user@example.com', pass: 'app-password' },
tls: { rejectUnauthorized: true },
});
async function sendEmail(to, subject, text, html = null, attachments = []) {
await transporter.sendMail({
from: '"StackPractices" <noreply@example.com>',
to,
subject,
text,
html,
attachments: attachments.map((file) => ({
path: file,
filename: path.basename(file),
})),
});
}
async function sendOrderConfirmation(to, context) {
const text = `Hola ${context.name}, tu orden ${context.orderId} está confirmada.`;
const html = `<p>Hola ${context.name}, tu orden <b>${context.orderId}</b> está confirmada.</p>`;
await sendEmail(to, `Orden #${context.orderId} confirmada`, text, html);
}
// Uso
sendOrderConfirmation('user@example.com', { name: 'Alice', orderId: '12345' });
Java
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.io.File;
import java.util.Properties;
public class SmtpSender {
public static void send(String host, int port, String user, String pass,
String to, String subject, String text, String html,
File[] attachments) throws Exception {
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", port);
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, pass);
}
});
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(user));
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
msg.setSubject(subject);
Multipart mixed = new MimeMultipart("mixed");
Multipart alt = new MimeMultipart("alternative");
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(text);
alt.addBodyPart(textPart);
if (html != null) {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(html, "text/html; charset=utf-8");
alt.addBodyPart(htmlPart);
}
MimeBodyPart wrapper = new MimeBodyPart();
wrapper.setContent(alt);
mixed.addBodyPart(wrapper);
if (attachments != null) {
for (File f : attachments) {
MimeBodyPart att = new MimeBodyPart();
DataSource source = new FileDataSource(f);
att.setDataHandler(new DataHandler(source));
att.setFileName(f.getName());
mixed.addBodyPart(att);
}
}
msg.setContent(mixed);
Transport.send(msg);
}
public static void main(String[] args) throws Exception {
send(
"smtp.example.com",
587,
"user@example.com",
"app-password",
"user@example.com",
"Orden #12345 confirmada",
"Hola Alice, tu orden 12345 está confirmada.",
"<p>Hola Alice, tu orden <b>12345</b> está confirmada.</p>",
null
);
}
}
Explicación
SMTP funciona como una breve negociación entre tu cliente y el servidor de correo. El cliente abre
una conexión por el puerto 587 (STARTTLS) o 465 (TLS implícito), inicia sesión con un usuario y una
contraseña de app o un token OAuth2, y luego construye y envía un mensaje MIME. La entrega real usa
la secuencia MAIL FROM → RCPT TO → DATA. A partir de ahí, el servidor responde con códigos de
estado; tu trabajo es manejar rebotes, reintentos y límites de tasa por separado.
A alto volumen, no llames a SMTP directamente desde un handler HTTP, porque eso acopla tu API a la velocidad del servidor de correo y puede causar timeouts. En su lugar, pasa los mensajes a una cola y dejá que los workers hagan el envío. Esto mantiene los tiempos de respuesta bajos y te da un lugar limpio para reintentar fallos transitorios.
Variantes
En Python, combina smtplib con el paquete email cuando necesites MIME personalizado. Node.js
suele usar nodemailer para apps full-stack, SSR y soporte rápido de adjuntos. En Java,
Jakarta Mail es la opción estándar para enterprise o Spring Boot.
Go y C# también merecen consideración. En Go, net/smtp y el paquete jordan-wright/email
funcionan bien para servicios de baja latencia, mientras que los desarrolladores .NET suelen
elegir MailKit.
Un ejemplo mínimo en Go con net/smtp:
package main
import (
"net/smtp"
"strings"
)
func sendEmail(host, port, user, pass, to, subject, body string) error {
addr := host + ":" + port
auth := smtp.PlainAuth("", user, pass, host)
msg := strings.Join([]string{
"From: " + user,
"To: " + to,
"Subject: " + subject,
"",
body,
}, "\r\n")
return smtp.SendMail(addr, auth, user, []string{to}, []byte(msg))
}
Para adjuntos y HTML en Go, pasa a github.com/jordan-wright/email, que maneja la
construcción MIME multipart por vos.
Mejores Prácticas
Empieza por lo básico: usa contraseñas de app o tokens OAuth2, y nunca las commitees en el control de versiones. Guárdalas en un gestor de secretos y rótalas periódicamente. Después añade los registros DNS SPF, DKIM y DMARC a tu dominio de envío. Estos tres registros tienen más impacto en la entregabilidad que cualquier otra cosa que puedas hacer.
Incluye siempre un fallback de texto plano junto con cada mensaje HTML. Algunos clientes, especialmente relojes y lectores de terminal, solo renderizan texto. Limita la tasa de envíos, ya que la mayoría de proveedores te limitan a unos pocos cientos de emails por minuto. Un dominio de envío dedicado también vale la pena: si una campaña daña su reputación, tu dominio principal queda limpio.
Por el lado operativo, maneja rebotes y quejas de forma asíncrona. Lee respuestas SMTP y eventos webhook para mantener la lista limpia. Por último, sanitiza cualquier entrada de usuario que termine en una plantilla o línea de asunto; la inyección de headers y los enlaces maliciosos son riesgos reales.
Errores Comunes
- Enviar desde localhost o un servidor compartido sin registros SPF/DKIM válidos. Tus mensajes van directo a la carpeta de spam.
- Meter credenciales de producción en el código o en archivos de entorno que se suben a control de versiones.
- Omitir la validación de direcciones de destinatario antes de encolar. Consulta validación de entrada para patrones seguros.
- Enviar correo de forma síncrona dentro de handlers HTTP, donde puede haber timeouts bajo carga.
- Ignorar límites de tasa y reintentos, lo que termina en bloqueos temporales o blacklist.
- Usar
TooCcpara envíos masivos. UsaBcco mensajes individuales para proteger a los destinatarios. - Correr sin logs ni monitoreo. Sin ellos, solo te enterás de los problemas de entrega cuando los usuarios se quejan.
Resumen
SMTP envía email a través de una negociación: conectá en 587 (STARTTLS) o 465 (TLS implícito),
autenticá con contraseñas de app o OAuth2, construí un mensaje MIME y enviá vía MAIL FROM →
RCPT TO → DATA. Siempre acompañá el HTML con un fallback de texto plano, y limitá la tasa de
envíos para no exceder los límites del proveedor. Autenticá tu dominio con SPF, DKIM y DMARC antes
de enviar nada, porque nada más mejora tanto la entregabilidad. No envíes correo de forma
síncrona desde handlers HTTP; pasá los mensajes a una cola y dejá que los workers manejen el envío
y los reintentos. Manejá rebotes y quejas de forma asíncrona vía webhooks. Sanitizá cualquier
entrada de usuario que termine en plantillas o asuntos para prevenir inyección de headers.
See Also
- Python smtplib: protocolo cliente SMTP.
- nodemailer: librería de envío de email para Node.js.
- Jakarta Mail: referencia de la API de email de Java.
- RFC 5321: especificación del protocolo SMTP.
- Google Postmaster Tools: monitoreo de reputación de remitente.
Preguntas frecuentes
¿Debería usar SMTP o una API de email transaccional?
SMTP tiene más sentido cuando valoras la portabilidad y podrías cambiar de proveedor más adelante. Una API de email transaccional como SendGrid, Mailgun o Postmark es mejor cuando quieres plantillas gestionadas, analytics y webhooks de rebotes listos para usar. La mayoría de proveedores soportan ambas, así que no quedás lockeado en un enfoque.
¿Cómo evito que mis emails terminen en spam?
El primer paso es configurar SPF, DKIM y DMARC en tu dominio de envío. Envía desde una dirección consistente, mantén el HTML limpio y adaptado a móviles, evita palabras spam en los asuntos y revisá tu reputación de remitente en Google Postmaster Tools. La interacción también importa: eliminá direcciones inactivas de tu lista con regularidad.
¿Puedo enviar newsletters masivas vía SMTP?
Técnicamente puedes, pero las newsletters masivas no son el punto fuerte de SMTP. Funciona genial para email transaccional y lotes pequeños. Para decenas de miles de destinatarios, usa una plataforma dedicada de email marketing y conéctala a tu aplicación a través de su API o importaciones de lista.
¿Cómo manejo adjuntos entre lenguajes?
El paquete email.mime de Python te permite construir partes MIMEBase y codificarlas en base64.
En Node, nodemailer toma un array attachments con campos path o content. En Java, Jakarta Mail te da MimeBodyPart con un DataHandler que envuelve un FileDataSource. Los tres manejan
archivos binarios de la misma forma por debajo: codifican el payload en base64 y setean
Content-Disposition a attachment. Mantené los adjuntos por debajo de 10 MB por archivo, o vas
a rebotar contra los límites del proveedor.
¿Cuál es la diferencia entre el puerto 587 y 465?
El puerto 587 usa STARTTLS, que comienza en texto plano y sube a TLS después de que el cliente envía el comando EHLO. El puerto 465 usa TLS implícito, cifrando desde el inicio. La mayoría de los proveedores soportan ambos, pero 587 es el default moderno porque lo requiere el RFC 6409. Si tu proveedor tiene 465, usalo para simplicidad — TLS arranca inmediatamente, sin paso de upgrade de protocolo.
Recursos Relacionados
Llamar a una API REST: Python, JS, Java y Go
Cómo hacer peticiones HTTP a una API REST y manejar la respuesta JSON en Python, JavaScript, Java y Go.
RecipeValidación de Input: Schemas en 3 Lenguajes
Valida el input de usuarios en el límite de la API con librerías de schemas. Ejemplos con Pydantic, Zod y Jakarta Validation, sanitización y errores estructurados.
RecipeGestionar Secretos de Aplicaciones de Forma Segura
Cómo almacenar, rotar e inyectar API keys, contraseñas de base de datos y certificados sin hardcodearlos en código fuente o archivos de entorno.
RecipeLimitación de Tasa (Rate Limiting)
Cómo implementar rate limiting en APIs usando token bucket, sliding window y fixed window en Python, JavaScript y Java.
RecipeManejar Errores en APIs con RFC 7807
Patrones para un manejo de errores de API consistente y predecible en varios lenguajes y frameworks.
RecipeLogging en producción con Python, JavaScript y Java
Implementa logging estructurado por niveles con IDs de correlación y enmascarado de secretos en Python, JavaScript y Java.