Sending Emails
Table of Contents
Why Sending Emails Matters in Backend Development
Most real applications need to send emails. Examples:
- Signup confirmation emails
- Password reset links
- Order confirmations and invoices
- Notifications (comment replies, status changes, alerts)
Backend code is usually responsible for triggering and sending these emails, often using an external email service.
This chapter focuses on how to actually send emails from backend code, what information an email needs, and how to avoid common pitfalls. It assumes you already understand the basics of “How Email Works” and “SMTP”.
Key idea: Your backend rarely sends email “directly” to the recipient. It usually hands the email to an SMTP server or email API, which then delivers it.
Core Concepts: What You Need To Send an Email
To send an email from a backend application, you always need three things:
- Transport configuration
How and where to send the email, for example: - SMTP server hostname, port, username, password
- Or an HTTP API endpoint and API key (like SendGrid, Mailgun)
- Message data
The content and metadata of the email: - From address
- To addresses
- Subject
- Body (plain text and/or HTML)
- Optional: CC, BCC, Reply-To, attachments, headers
- Client library or implementation
Something that connects to the SMTP server or email API and passes the message.
A simple mental model:
| Step | Question | Example answer |
|---|---|---|
| 1 | Where do I send this email? | smtp.gmail.com:587 with user and password |
| 2 | What is the email content? | From: app@example.com, To: you@example.com |
| 3 | How do I send it? | Use smtplib in Python, or a service’s SDK |
Email Message Structure in Practice
An email message can be split into:
- Headers: metadata
- Body: the actual content, which can be:
- Plain text
- HTML
- Multipart (both text and HTML, plus attachments)
Example raw email structure (simplified):
From: "My App" <no-reply@myapp.com>
To: alice@example.com
Subject: Welcome to My App!
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Hello Alice,
Welcome to My App!
Thanks,
The My App TeamFor HTML:
Content-Type: text/html; charset="utf-8"
<html>
<body>
<p>Hello <b>Alice</b>,</p>
<p>Welcome to <strong>My App</strong>!</p>
</body>
</html>Most real emails are multipart and include both text and HTML so that:
- Modern clients show HTML.
- Text-only clients still show something readable.
Sending Emails with SMTP (Conceptual Flow)
When your backend sends an email via SMTP, this is the simplified sequence:
- Open a connection to the SMTP server (for example
smtp.example.comon port587). - Start TLS encryption if required.
- Authenticate with a username and password or special SMTP credentials.
- Provide sender and recipients.
- Send the message content.
- Close the connection.
In pseudocode:
connect_to_smtp(server, port)
use_tls()
login(username, password)
send_mail(from_address, to_addresses, message)
disconnect()Important rule: Never hard-code SMTP passwords or API keys in your code. Store them in environment variables or a secrets manager.
A Minimal Python SMTP Example
This example shows a simple email send using Python’s standard library. In a real project you would move this into a function or a dedicated module.
import smtplib
from email.message import EmailMessage
SMTP_HOST = "smtp.example.com"
SMTP_PORT = 587
SMTP_USER = "smtp-user"
SMTP_PASSWORD = "smtp-password"
def send_welcome_email(recipient_email: str) -> None:
msg = EmailMessage()
msg["From"] = "My App <no-reply@myapp.com>"
msg["To"] = recipient_email
msg["Subject"] = "Welcome to My App!"
msg.set_content(
"Hello,\n\n"
"Welcome to My App!\n\n"
"Thanks,\n"
"The My App Team"
)
# Optional HTML alternative
msg.add_alternative(
"""\
<html>
<body>
<p>Hello,</p>
<p>Welcome to <strong>My App</strong>!</p>
<p>Thanks,<br>The My App Team</p>
</body>
</html>
""",
subtype="html"
)
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
smtp.starttls()
smtp.login(SMTP_USER, SMTP_PASSWORD)
smtp.send_message(msg)Key points:
EmailMessagetakes care of proper MIME formatting.set_contentsets the plain text.add_alternativeadds HTML, so the email becomes multipart.starttls()ensures data is encrypted between your app and the SMTP server.
Using an Email Service Provider (API-Based)
Many production systems use Email Service Providers (ESPs) instead of raw SMTP, for example:
- SendGrid
- Mailgun
- Amazon SES
- Postmark
Common advantages:
- Better deliverability
- Tracking (opens, clicks, bounces)
- Email templates
- Rate limiting and scaling
- Web dashboards and logs
Typical workflow:
- Create an account and verify your domain.
- Get an API key.
- Use the provider’s HTTP API or SDK from your backend.
A very generic example of sending an email via a fictitious email API:
import requests
API_KEY = "your-api-key"
API_URL = "https://api.emailservice.com/v1/send"
def send_reset_email(to_email: str, reset_link: str) -> None:
data = {
"from": "My App <no-reply@myapp.com>",
"to": [to_email],
"subject": "Reset your password",
"text": f"Click this link to reset your password: {reset_link}",
"html": f"<p>Click <a href='{reset_link}'>this link</a> to reset your password.</p>",
}
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
response = requests.post(API_URL, json=data, headers=headers)
response.raise_for_status()You do not need to manage SMTP details here. The provider handles them.
Generating Dynamic Email Content
Most application emails contain dynamic data, for example:
- User name
- Order details
- One-time tokens
- Custom messages
Instead of building strings by hand, you usually use templates, similar to HTML templates for web pages.
Simple string formatting:
body = (
"Hello {name},\n\n"
"Your order #{order_id} has been shipped.\n"
"Track it here: {tracking_url}\n\n"
"Thanks,\n"
"The Store Team"
).format(
name=user_name,
order_id=order_id,
tracking_url=tracking_url,
)Template example using a Jinja2-style template (conceptually):
<!-- order_shipped_email.html -->
<p>Hello {{ name }},</p>
<p>Your order <strong>#{{ order_id }}</strong> has been shipped.</p>
<p>You can track it here: <a href="{{ tracking_url }}">{{ tracking_url }}</a></p>
<p>Thanks,<br>The Store Team</p>Pseudocode rendering:
html_body = template_engine.render(
"order_shipped_email.html",
{"name": name, "order_id": order_id, "tracking_url": tracking_url},
)This keeps your email designs out of your Python code and makes them easier to edit.
Handling Common Email Fields
Backend code should know when and how to use these fields:
| Field | Purpose | Example |
|---|---|---|
| From | Sender address, often no-reply | My App <no-reply@myapp.com> |
| To | Main recipient(s) | alice@example.com |
| CC | Visible copy to others | manager@example.com |
| BCC | Hidden copy to others | audit@example.com |
| Reply-To | Address that gets replies | support@myapp.com |
| Subject | Short summary | Your order has shipped |
Example of adding CC and BCC with EmailMessage:
msg["From"] = "My App <no-reply@myapp.com>"
msg["To"] = "alice@example.com"
msg["Cc"] = "manager@example.com"
msg["Bcc"] = "audit@example.com"
msg["Subject"] = "Monthly report"
Note that some libraries treat BCC separately. For SMTP, recipients in Bcc must still be passed to the SMTP client, even if the header is not visible in the message.
Attachments (High-Level Overview)
Sometimes you need to send files:
- PDF invoices
- CSV exports
- Images
Conceptually:
- Load file content.
- Attach it to the email with a MIME type and a filename.
Python example:
from email.message import EmailMessage
from pathlib import Path
def send_invoice_email(to_email: str, invoice_path: Path) -> None:
msg = EmailMessage()
msg["From"] = "Billing <billing@myapp.com>"
msg["To"] = to_email
msg["Subject"] = "Your invoice"
msg.set_content("Please find your invoice attached.")
file_bytes = invoice_path.read_bytes()
msg.add_attachment(
file_bytes,
maintype="application",
subtype="pdf",
filename=invoice_path.name,
)
# Send via SMTP (omitting boilerplate for brevity)Make sure you respect file size limits and security rules from the “Working with Files” and “File Upload Security” topics.
Testing Email Sending During Development
Sending real emails every time during development is slow and can be unsafe. Use these approaches:
- Local SMTP debug server
Many languages and frameworks allow you to run a dummy SMTP server that prints emails to the console or a UI instead of delivering them.
Example with Python (from command line):
python -m smtpd -c DebuggingServer -n localhost:1025Then configure your app:
SMTP_HOST = "localhost"
SMTP_PORT = 1025- Sandbox mode / test mode
Many email providers offer a sandbox mode where emails are not actually sent to real inboxes. - Log-only mode
For development, you can log email content instead of sending it:
def send_email(msg: EmailMessage) -> None:
if settings.ENV == "development":
print("EMAIL WOULD BE SENT:")
print(msg)
return
# production sending logic hereImportant rule: Never send real user emails for automated tests or local development. Use test addresses or sandbox modes.
Handling Errors and Retries
Email sending can fail for several reasons:
- Network issues
- Wrong credentials
- Provider limits or errors
- Invalid recipient addresses
In code you should:
- Catch exceptions around the send call.
- Log the error with enough context.
- Decide whether to:
- Show a friendly message to the user.
- Queue the email for retry in a background job.
- Mark email as failed in the database.
Simple pattern:
import logging
logger = logging.getLogger(__name__)
def send_email_safe(msg: EmailMessage) -> bool:
try:
# send via SMTP or API here
return True
except Exception as exc:
logger.exception("Failed to send email to %s", msg["To"])
return FalseIn a production system, email sending is frequently moved to background jobs, so slow or failing email does not block user-facing requests. That connects directly to the “Background Processing” section in this course.
Configuration and Environment Separation
You will often run your application in different environments:
developmentstagingproduction
Each environment can have different:
- SMTP host and port.
- Credentials.
- Sender email addresses.
- Email provider accounts.
Typical configuration table:
| Environment | SMTP/Provider | Example from address |
|---|---|---|
| Dev | Local debug SMTP | dev-no-reply@myapp.local |
| Staging | Provider sandbox account | staging@myapp.com |
| Prod | Provider production account | no-reply@myapp.com |
Keep them in environment variables:
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_USER=myapp-smtp-user
EMAIL_PASSWORD=super-secret
EMAIL_FROM="My App <no-reply@myapp.com>"Then load them in code. This keeps secrets out of your repository and makes deployments configurable.
Putting It All Together: A Simple Email Service Class
This is a more structured example you might use in a backend project:
import smtplib
from email.message import EmailMessage
class EmailService:
def __init__(self, host: str, port: int, user: str, password: str, default_from: str):
self.host = host
self.port = port
self.user = user
self.password = password
self.default_from = default_from
def send(
self,
to: list[str],
subject: str,
text_body: str,
html_body: str | None = None,
from_address: str | None = None,
) -> None:
msg = EmailMessage()
msg["From"] = from_address or self.default_from
msg["To"] = ", ".join(to)
msg["Subject"] = subject
msg.set_content(text_body)
if html_body:
msg.add_alternative(html_body, subtype="html")
with smtplib.SMTP(self.host, self.port) as smtp:
smtp.starttls()
smtp.login(self.user, self.password)
smtp.send_message(msg)
# Usage example
email_service = EmailService(
host="smtp.example.com",
port=587,
user="smtp-user",
password="smtp-password",
default_from="My App <no-reply@myapp.com>",
)
email_service.send(
to=["alice@example.com"],
subject="Welcome!",
text_body="Hello Alice,\nWelcome to My App!",
html_body="<p>Hello <b>Alice</b>,</p><p>Welcome to My App!</p>",
)This keeps email sending logic in one place and makes it easier to:
- Switch from SMTP to an email provider API later.
- Mock the service in tests.
- Centralize logging and error handling.
Summary
In backend applications, sending emails involves:
- Configuring an SMTP server or an email provider API.
- Building structured email messages with headers, text, HTML, and sometimes attachments.
- Using templates to generate dynamic content.
- Separating configuration per environment and keeping secrets safe.
- Testing email sending without spamming real users.
- Handling errors and, in production, moving email sending to background jobs.
With these concepts, you can implement welcome emails, password resets, order confirmations, and other common email features in backend systems.
Views: 8
KAHIBARO