Send email with Django SMTP through Volanea by configuring Django’s built-in SMTP backend with the relay credentials supplied for your Volanea account. This guide uses Django’s standard mail utilities—there is no Volanea-specific Python SDK required for SMTP delivery.
What this integration uses
Django ships with an SMTP email backend. It opens an authenticated connection to the SMTP host configured in your settings, hands the message to that server, and returns control to your application once the SMTP transaction succeeds or fails. That makes SMTP a practical choice when you already use Django’s send_mail, EmailMessage, or EmailMultiAlternatives APIs.
Volanea supports SMTP relay sending for transactional mail. In this setup, Django is responsible for composing the email, while Volanea handles the relay connection and downstream delivery pipeline. The integration does not call a fictional provider SDK method or require an HTTP request from your application.
You will need the SMTP connection values shown for your Volanea account:
- SMTP host
- SMTP port
- SMTP username
- SMTP credential or API key used as the SMTP password
- TLS or SSL connection mode
- A sender address on a domain you have configured for sending
Do not guess the relay hostname, port, or authentication format. Copy the SMTP values supplied in your account or confirm them in the email API reference and setup guides. SMTP providers can support different ports, security modes, and credentials, and those values must match exactly.
Before you start
This guide assumes that you have a Django project and Python available locally or in your deployment environment. It is written for Django 5.x, whose EMAIL_* SMTP settings are widely used in existing Django applications.
Before running the sample, make sure the following are true:
- Your Volanea account has SMTP credentials available for the sending environment you are configuring.
- Your From address belongs to a sender domain that is configured for use with Volanea.
- Your deployment platform allows outbound TCP traffic to the SMTP port you selected.
- You have a real recipient address available for a controlled test.
- You will store credentials in environment variables or a secrets manager, rather than committing them to source control.
A successful SMTP submission means the relay accepted the message. It is not the same as an inbox placement guarantee: recipient-server acceptance, spam filtering, bounces, and complaints occur later in the delivery lifecycle. Keep that distinction in mind when you test and monitor a production integration.
Install Django
Django’s SMTP backend is included with Django itself. You do not need to install a separate Volanea SDK or a separate SMTP client library for this guide.
Install Django with pip:
python -m pip install "Django>=5.2,<6.0"
If you already have a Django project with a compatible version installed, skip that command. To create a minimal project for testing, run:
django-admin startproject volanea_django
cd volanea_django
The project created by startproject includes a settings.py file. That is where the SMTP backend configuration belongs. You can send mail from a view, a management command, a background worker, a signal handler, or application service code; the SMTP settings remain centralized.
Why no provider SDK is installed
SMTP is a standard protocol, and Django already implements an SMTP email backend. Adding an unneeded provider package creates another dependency without improving the core send path. The relevant integration work is therefore credential management, secure transport configuration, message composition, sender authentication, observability, and error handling.
If you later choose Volanea’s REST API instead of SMTP, that is a separate integration approach. Do not combine SMTP credentials and API request syntax in the same implementation unless the relevant Volanea documentation explicitly says to do so.
Set environment variables
Use environment variables for every secret and deployment-specific SMTP value. The example calls the SMTP password variable VOLANEA_SMTP_API_KEY because many platforms expose a key-like credential for SMTP authentication. Use the exact SMTP credential Volanea provides; do not assume that a REST API key is automatically valid for SMTP login.
For a local shell session, set the values before starting Django. Replace every placeholder with your own values.
export VOLANEA_SMTP_HOST="your-volanea-smtp-host"
export VOLANEA_SMTP_PORT="587"
export VOLANEA_SMTP_USERNAME="your-volanea-smtp-username"
export VOLANEA_SMTP_API_KEY="your-volanea-smtp-credential"
export VOLANEA_SMTP_USE_TLS="true"
export VOLANEA_DEFAULT_FROM_EMAIL="Billing <billing@your-verified-domain.example>"
export TEST_RECIPIENT_EMAIL="you@example.com"
The sample uses STARTTLS-style configuration: a normal SMTP connection is established first, then upgraded with TLS. Port 587 is a common SMTP submission port, but it is only an example here. If your Volanea SMTP details specify another port or implicit SSL, use the documented values for that credential set.
Never place a real credential in settings.py, a committed .env file, browser-delivered JavaScript, screenshots, issue trackers, or logs. In production, set the same values through your host’s encrypted environment-variable or secret-management facility.
Keep SMTP credentials separate by environment
Use different credentials or at least distinct secrets for local development, staging, and production whenever your account configuration permits it. That lets you revoke a compromised development secret without interrupting production sending, and it makes it easier to trace a send back to the deployment that made it.
Avoid using a personal mailbox password as an application SMTP password. Application credentials should be scoped to the sending service and rotated when access changes. Store only the secret identifier or credential name in internal documentation; store the secret value in a proper secrets manager.
Configure Django’s SMTP backend
Open volanea_django/settings.py and add the following block near the end of the file. This code reads values from environment variables, validates that required values exist, and configures Django’s built-in SMTP backend.
# volanea_django/settings.py
import os
def required_env(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = required_env("VOLANEA_SMTP_HOST")
EMAIL_PORT = int(os.environ.get("VOLANEA_SMTP_PORT", "587"))
EMAIL_HOST_USER = required_env("VOLANEA_SMTP_USERNAME")
EMAIL_HOST_PASSWORD = required_env("VOLANEA_SMTP_API_KEY")
EMAIL_USE_TLS = os.environ.get("VOLANEA_SMTP_USE_TLS", "true").lower() == "true"
EMAIL_USE_SSL = False
EMAIL_TIMEOUT = 20
DEFAULT_FROM_EMAIL = required_env("VOLANEA_DEFAULT_FROM_EMAIL")
SERVER_EMAIL = DEFAULT_FROM_EMAIL
Django uses EMAIL_HOST and EMAIL_PORT to find the relay. When EMAIL_HOST_USER and EMAIL_HOST_PASSWORD are set, Django supplies them during SMTP authentication. EMAIL_USE_TLS=True requests STARTTLS; it must not be enabled together with EMAIL_USE_SSL=True.
EMAIL_TIMEOUT is intentional. Without a timeout, a network or relay problem can tie up a web request or worker for longer than your application can tolerate. Twenty seconds is a reasonable starting point for a synchronous transactional send, but tune it to match your platform, retry behavior, and user experience.
STARTTLS versus implicit SSL
SMTP security configuration is not interchangeable. There are two common models:
- STARTTLS: Connect to the SMTP service and upgrade the connection using TLS. In Django, set
EMAIL_USE_TLS=TrueandEMAIL_USE_SSL=False. - Implicit SSL/TLS: Start the connection inside TLS from the beginning. In Django, set
EMAIL_USE_SSL=TrueandEMAIL_USE_TLS=False.
Use the model and port documented for your Volanea SMTP credentials. Enabling both settings is invalid. Using the wrong model can produce connection resets, TLS handshake errors, or confusing authentication failures because the server and client are speaking different protocols at connection time.
Send one transactional email
Create a Django management command so the example can be run without building an HTTP endpoint. This is safer for an initial integration test because you can run the send deliberately and inspect the terminal output.
Create these directories and empty __init__.py files:
volanea_django/
├── manage.py
├── volanea_django/
│ ├── settings.py
│ └── ...
└── core/
└── management/
└── commands/
└── send_test_email.py
Create the app first if you do not already have one:
python manage.py startapp core
Then add "core" to INSTALLED_APPS in settings.py. Next, create core/management/commands/send_test_email.py with this complete code:
import os
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.core.management.base import BaseCommand, CommandError
from django.utils.html import strip_tags
class Command(BaseCommand):
help = "Send a transactional test email through the configured SMTP backend."
def handle(self, *args, **options):
recipient = os.environ.get("TEST_RECIPIENT_EMAIL")
if not recipient:
raise CommandError("Set TEST_RECIPIENT_EMAIL before running this command.")
subject = "Your Volanea SMTP test email"
html_body = """
<!doctype html>
<html lang="en">
<body>
<h1>SMTP is configured</h1>
<p>This transactional email was sent by Django through Volanea.</p>
<p>If you received it, check its headers and delivery event data as part of your test.</p>
</body>
</html>
"""
text_body = strip_tags(html_body)
message = EmailMultiAlternatives(
subject=subject,
body=text_body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[recipient],
)
message.attach_alternative(html_body, "text/html")
try:
delivered_messages = message.send(fail_silently=False)
except Exception as exc:
raise CommandError(f"SMTP send failed: {exc}") from exc
if delivered_messages != 1:
raise CommandError(
f"Expected Django to send one message, but got {delivered_messages}."
)
self.stdout.write(
self.style.SUCCESS(
f"SMTP submission accepted for {recipient} using {settings.EMAIL_HOST}:{settings.EMAIL_PORT}."
)
)
Run the command:
python manage.py send_test_email
The command sends both a plain-text body and an HTML alternative. That is important because some recipients, clients, security tools, and accessibility workflows rely on plain text. EmailMultiAlternatives creates a multipart email instead of sending HTML as unlabelled plain text.
The message.send() return value is normally the number of messages sent by Django’s backend. This example expects 1; an exception or unexpected result makes the command exit as a failure. In application code, preserve the exception context and include a safe correlation identifier in logs, but never log SMTP passwords or full sensitive recipient data.
Use the same configuration in a view or service
A management command is useful for validation, but production transactional email often follows an application event: a password reset request, a receipt, a verification step, or an account notification. Keep message construction in a dedicated service instead of embedding SMTP logic throughout views and models.
Here is a reusable service function:
# core/services/email.py
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
def send_welcome_email(recipient: str, first_name: str) -> int:
subject = "Welcome to Example App"
html_body = f"""
<html>
<body>
<h1>Welcome, {first_name}!</h1>
<p>Your account is ready to use.</p>
</body>
</html>
"""
message = EmailMultiAlternatives(
subject=subject,
body=strip_tags(html_body),
from_email=settings.DEFAULT_FROM_EMAIL,
to=[recipient],
)
message.attach_alternative(html_body, "text/html")
return message.send(fail_silently=False)
Do not interpolate untrusted user input directly into HTML with an f-string as a general template strategy. The compact example above is acceptable only when first_name is trusted or escaped before rendering. In a real application, use Django templates with autoescaping enabled, validate address inputs, and keep business logic separate from presentation.
For email that is part of a database workflow, consider sending after the transaction commits. A confirmation email for an account, order, or invoice should not be delivered for a database transaction that later rolls back. Django’s transaction.on_commit() can help coordinate that boundary.
Verify the sender and message format
The From address is not merely cosmetic. It is part of the identity recipients see, and it should belong to a domain configured for sending through your email provider. Use a stable transactional address such as notifications@yourdomain.example or billing@yourdomain.example, rather than changing the sender for every request.
A well-formed transactional message should generally include:
- A clear, non-empty subject line.
- A valid From address from your configured sending domain.
- One or more recipient addresses supplied as a list to Django.
- A plain-text body.
- An HTML alternative when you use HTML email.
- Content that matches the user action that triggered the send.
- No secrets, reset tokens, or personal data in logs.
Do not set the From address equal to an arbitrary end user’s address. If your product needs replies to reach a customer or agent, use Django’s reply_to support with an address you have validated and a deliberate handling process. Sender spoofing or misaligned identity can cause provider rejection or damage trust with recipients.
Test the message, not just the function call
After the command reports success, inspect the message in the recipient mailbox. Confirm the visible sender, subject, plain-text rendering, HTML rendering, links, and reply behavior. If you can inspect raw headers, confirm that the message was relayed as expected and that your configured sender domain is represented correctly.
Also test a failure case in a non-production environment: temporarily use an invalid credential or a blocked test host and ensure your application surfaces a controlled error. The goal is not to force an outage, but to make sure the team can distinguish configuration errors from a successful send and can diagnose a failure without leaking secrets.
Common errors
SMTP authentication fails
Authentication failures commonly appear as an SMTP authentication exception, a 535-style response, or a generic login failure. Start by checking VOLANEA_SMTP_USERNAME and VOLANEA_SMTP_API_KEY for whitespace, an accidental quote character, credentials copied from the wrong environment, or a revoked secret.
The value in EMAIL_HOST_PASSWORD must be the credential intended for SMTP authentication. A REST API key, dashboard login password, or another provider’s SMTP password may not work. Re-copy the current SMTP username and credential from the Volanea configuration for the account and environment you are using, then restart the process so it receives the updated environment.
TLS, SSL, or port mismatch
A timeout, disconnect, WRONG_VERSION_NUMBER, or handshake error often points to a transport mismatch. Check that the host, port, and security mode are a matched set from the same SMTP setup instructions.
For STARTTLS, use EMAIL_USE_TLS=True and EMAIL_USE_SSL=False. For implicit SSL, use EMAIL_USE_SSL=True and EMAIL_USE_TLS=False. Do not set both flags to true, and do not assume a common port is correct if your Volanea SMTP credentials document another value.
Django sends to the console instead of Volanea
Django projects may use a console or in-memory email backend during development and tests. If no email leaves your process, inspect EMAIL_BACKEND and search your settings modules for overrides such as django.core.mail.backends.console.EmailBackend or django.core.mail.backends.locmem.EmailBackend.
Also check whether environment-specific settings are loaded. It is common to edit one settings.py file while DJANGO_SETTINGS_MODULE points to a different production or development settings module.
Wrong content type or HTML displayed as text
SMTP does not use an HTTP Content-Type request header, so do not try to fix this by adding REST headers to a Django SMTP configuration. The message itself needs MIME alternatives.
Use EmailMultiAlternatives, supply the plain-text body as body, then call attach_alternative(html_body, "text/html"). Passing HTML to send_mail() as the main text message without html_message, or using EmailMessage without an HTML alternative, can cause recipients to see markup as plain text.
The sender address is rejected
A relay may reject mail when the From address is not permitted for your configured sending domain. Confirm that VOLANEA_DEFAULT_FROM_EMAIL uses an address and domain you are authorized to send from, and that you did not accidentally deploy an example address such as billing@your-verified-domain.example.
Sender configuration is separate from SMTP login. Correct credentials do not make arbitrary domains valid sender identities. Configure the domain first, then use a consistent sender address from that domain.
Connection timeout or network unreachable
If Django waits until EMAIL_TIMEOUT expires, the issue may be network egress rather than credentials. Cloud hosts, corporate networks, containers, and serverless platforms sometimes block outbound SMTP ports or require explicit firewall rules.
Test from the same runtime environment that runs Django, not only from a laptop. Verify the configured hostname resolves there, the selected TCP port is allowed, and a proxy or inspection device is not replacing or blocking the TLS connection.
Errors are silently ignored
fail_silently=True suppresses SMTP exceptions. It can be appropriate only when you intentionally handle a failed send elsewhere, but it is a poor default for transactional messages that users depend on.
Use fail_silently=False while implementing and testing, as in this guide. In production, catch expected exceptions at a service boundary, record a safe error event, decide whether the action should be retried, and return an appropriate application response. Do not treat a suppressed exception as confirmation that email was sent.
Async code does not await the send correctly
Django’s built-in SMTP send APIs are synchronous. Calling message.send() from an async def view still performs blocking SMTP work unless you deliberately move it to a worker or bridge it safely to a thread.
Do not write await message.send(); it returns an integer, not an awaitable. For user-facing asynchronous applications, enqueue transactional email in a background job system after the relevant database transaction commits, or use a suitable sync-to-async boundary while understanding that SMTP network work is still blocking somewhere.
Duplicate sends after retries
SMTP submission can succeed while your application loses the connection or times out before it records success. A blind retry can therefore create duplicate messages. This is especially relevant for receipts, password-reset requests, and notifications triggered by webhook processing.
Use an application-level idempotency strategy: record the business event, generate a stable event identifier, and have workers claim or mark that event before sending. Log the event identifier—not the email body or API credential—so an operator can investigate suspected duplicates.
Production guidance for transactional sends
A direct SMTP call in a web request is simple, but it couples user-facing latency to DNS lookup, TCP connection, TLS negotiation, authentication, and remote relay availability. For low-volume, non-critical notifications, that may be acceptable. For signups, billing, security alerts, or any flow where retries matter, use a background worker.
A robust send flow often looks like this:
- Validate the business action and recipient address.
- Commit the business record or event to the database.
- Enqueue a job after commit with an idempotency key.
- Render a text and HTML message in the worker.
- Submit through Django’s configured SMTP backend.
- Record the SMTP submission attempt and safe diagnostic details.
- Use delivery events to understand what happened after relay acceptance.
This design makes it possible to retry transient network errors without holding an HTTP request open. It also creates a durable audit trail that distinguishes “we intended to send” from “the application submitted a message” and from “the recipient server later accepted or rejected it.”
Rate limiting and backoff should be explicit. If many jobs fail because of a temporary network or credential issue, unlimited immediate retries can amplify the problem and generate duplicate sends. Use capped retries with delay, alert on sustained failures, and stop retrying permanently invalid recipient or sender configurations.
Security and deliverability checklist
SMTP credentials grant the ability to send email through your account. Treat them with the same care as a production database password. Rotate credentials when an engineer leaves, when a secret leaks, and as part of a planned security policy.
Use this checklist before enabling a production flow:
- Store SMTP credentials only in encrypted deployment secrets.
- Restrict who can view or change production environment variables.
- Use a configured sender domain rather than a free-mailbox From address.
- Send multipart text and HTML content where HTML is used.
- Set reasonable SMTP connection timeouts.
- Keep
fail_silently=Falseunless your code has a specific error-handling path. - Avoid putting tokens, credentials, or full recipient details in logs.
- Test with real recipient inboxes before launching a high-impact flow.
- Add idempotency before introducing automatic retries.
- Monitor delivery, bounce, and complaint signals after launch.
Authentication and content quality are connected. Proper domain setup helps recipients identify your mail, but it does not make irrelevant or unexpected messages welcome. Transactional email should be timely, directly connected to a user action or account event, and sent only to an appropriate recipient.
Next steps
Once the basic SMTP send works, add operational feedback rather than treating submission as the end of the process. Webhooks can notify your application about email events so you can record delivery, bounce, complaint, open, or click information where that is relevant to your product and privacy model. Verify webhook signatures where supported, process events idempotently, and respond quickly with a successful HTTP status before moving expensive work to a queue.
For repeatable layouts, move message markup out of Python strings. Templates let your team maintain transactional designs, share headers and footers, render variables safely, and keep a consistent plain-text alternative. Django templates work well for application-owned emails; if you use provider-managed templates, follow the provider’s documented payload and variable rules rather than assuming SMTP supports template names itself.
If you are estimating production volume, credentials, or plan fit before launch, review transactional email sending costs. Keep SMTP configuration in code and infrastructure documentation, while keeping message content and sender identity decisions owned by the teams responsible for the customer experience.
FAQ
Do I need a Volanea Python SDK to send email with Django SMTP?
No. Django includes an SMTP backend, so this integration uses Django’s mail classes and Volanea SMTP credentials. Install Django, configure the SMTP settings, and send with EmailMultiAlternatives, EmailMessage, or send_mail.
Is VOLANEA_SMTP_API_KEY always the same as a REST API key?
Not necessarily. The variable name in this guide is a secure environment-variable label. Populate it only with the password or credential Volanea specifies for SMTP authentication, and use the associated SMTP username, host, port, and TLS mode from the same configuration.
Should I use EMAIL_USE_TLS or EMAIL_USE_SSL?
Use the security mode specified for your SMTP connection. STARTTLS uses EMAIL_USE_TLS=True and EMAIL_USE_SSL=False; implicit SSL uses EMAIL_USE_SSL=True and EMAIL_USE_TLS=False. Never enable both at the same time.
Why does Django report success but the email is not in the inbox?
A successful SMTP call usually means the relay accepted your message. The recipient server can still defer, reject, quarantine, filter, or route it elsewhere afterward. Check the recipient mailbox and message headers, then use delivery-event data to investigate the rest of the lifecycle.
Can I call await message.send() in an async Django view?
No. Django’s standard SMTP send method is synchronous and returns an integer. Queue email to a background worker or bridge synchronous work carefully; do not await the integer returned by message.send().