Send Transactional Emails with SMTP in Python, Node.js, Java
Learn to send transactional and bulk emails via SMTP with Python, Node.js, and Java. Covers auth, templates, attachments, rate limiting, and deliverability.
Overview
Most apps still fall back on email for password resets, order confirmations, newsletters, and alerts. SMTP (Simple Mail Transfer Protocol) is what moves those messages from your app to the recipient’s inbox. Using it directly keeps you portable between providers and gives you full control over templates, attachments, and sender reputation. This recipe covers production-ready patterns in Python, Node.js, and Java, with TLS, plain + HTML bodies, attachments, and rate throttling.
When to Use
- Your app sends transactional email and you want to swap providers without a rewrite.
- You want live data in your templates, or you prefer to keep email logic in your own code.
- You need to attach invoices, reports, or exports to outgoing messages.
- You already run a message queue or worker pool and want SMTP senders to pull jobs from it.
- You’re combining email with rate limiting and secret management best practices.
When to Avoid
- Your volume is millions of marketing emails per month. Dedicated platforms like Mailchimp, Brevo, and Mailgun handle list hygiene and unsubscribe compliance far better.
- Your team can’t keep up with DNS records (SPF, DKIM, DMARC), bounce handling, and sender reputation.
- In-app notifications need low latency. Push notifications or WebSockets fit those cases better.
Solution
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):
"""Build a multipart email with plain text, optional HTML, and attachments."""
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)
# Usage
text_template = "Hi $name, your order $order_id is confirmed."
html_template = "<p>Hi $name, your order <b>$order_id</b> is confirmed.</p>"
send_template_email(
to="user@example.com",
subject="Order #12345 confirmed",
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 = `Hi ${context.name}, your order ${context.orderId} is confirmed.`;
const html = `<p>Hi ${context.name}, your order <b>${context.orderId}</b> is confirmed.</p>`;
await sendEmail(to, `Order #${context.orderId} confirmed`, text, html);
}
// Usage
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",
"Order #12345 confirmed",
"Hi Alice, your order 12345 is confirmed.",
"<p>Hi Alice, your order <b>12345</b> is confirmed.</p>",
null
);
}
}
Explanation
SMTP works like a short negotiation between your client and the mail server. The client opens a
connection on port 587 (startTLS) or 465 (SSL/TLS), logs in with a username and app password or an
OAuth2 token, then builds and sends a MIME message. The actual delivery uses the sequence MAIL FROM → RCPT TO → DATA. After that, the server replies with status codes; your job is to handle
bounces, retries, and rate limits on the side.
High-volume sending shouldn’t happen inside an HTTP handler. That path couples your API to the speed of the mail server and can cause timeouts. A better design is to hand messages off to a queue and let workers handle delivery. This keeps response times low and gives you a clean place to retry transient failures.
Variants
For Python, pair smtplib with the email package when you need custom MIME handling. Node.js
tends to default to nodemailer for full-stack apps, SSR, and quick attachment support. Java
projects usually pick Jakarta Mail for enterprise or Spring Boot codebases.
Go and C# are also solid choices. Go’s net/smtp and the jordan-wright/email package work well for
low-latency services, while .NET developers usually reach for MailKit.
Best Practices
Start with the basics: use app passwords or OAuth2 tokens, and never commit them to version control. Store them in a secret manager and rotate them on a schedule. Next, add SPF, DKIM, and DMARC DNS records to your sending domain. No other change moves the needle on deliverability as much as these records.
Always send a plain text fallback with your HTML messages. Some clients, especially watches and terminal readers, only render text. Throttle your sends because most providers cap you at a few hundred emails per minute. A dedicated sending domain is also worth it: if a campaign damages its reputation, your main domain stays clean.
On the operational side, handle bounces and spam complaints asynchronously. Read SMTP responses and webhook events to keep your list clean. Finally, sanitize any user input that lands in a template or subject line; header injection and malicious links are real risks.
Common Mistakes
- Sending from localhost or a shared server without valid SPF/DKIM records. Your messages land in spam.
- Embedding production credentials in source code or in environment files that get checked into version control.
- Skipping recipient address validation before queuing. See input validation for safe patterns.
- Sending mail synchronously inside HTTP handlers, where it can time out under load.
- Ignoring rate limits and retries, which leads to temporary blocks or blacklisting.
- Using
ToorCcfor bulk sends. UseBccor individual messages to protect recipients. - Running without any logs or monitoring. Without them, you only notice delivery problems when users complain.
Frequently Asked Questions
Should I use SMTP or a transactional email API?
SMTP makes sense when you care about portability and might switch providers later. A transactional email API like SendGrid, Mailgun, or Postmark is better when you want managed templates, analytics, and bounce webhooks out of the box. Most providers give you both options, so the choice isn't always forced.
How do I stop emails from landing in spam?
The first step is to authenticate with SPF, DKIM, and DMARC. Send from a consistent address, keep the HTML clean and mobile-friendly, avoid spammy subject words, and watch your sender reputation with Google Postmaster Tools. Engagement also matters, so clean out inactive recipients from your list regularly.
Can I send bulk newsletters via SMTP?
Technically you can, but large newsletters aren't what SMTP is built for. It handles transactional email and small batches well. For tens of thousands of recipients, use a dedicated email marketing platform and connect it to your app through its API or list imports.
Related Resources
Call a REST API: Python, JavaScript, Java & Go Examples
How to make HTTP requests to a REST API and handle the JSON response in Python, JavaScript, Java, and Go.
RecipeInput Validation
How to validate user input safely using schemas, type checking, and sanitization across Python, JavaScript, and Java.
RecipeManage Application Secrets Securely
How to store, rotate, and inject API keys, database passwords, and certificates without hardcoding them in source code or environment files.
RecipeRate Limiting
How to implement API rate limiting using token bucket, sliding window, and fixed window algorithms across Python, JavaScript, and Java.
RecipeHandle API Errors with RFC 7807 and HTTP Status Codes
Patterns for consistent, predictable API error handling across multiple languages and frameworks.
RecipeLogging
How to implement structured, level-based logging across Python, JavaScript, and Java with what works for production observability.