Advanced Usage¶
This section covers advanced features and usage patterns of python-emails.
SMTP Connections¶
By default, send() accepts an smtp dict and
manages the connection internally:
response = message.send(
to="user@example.com",
smtp={"host": "smtp.example.com", "port": 587, "tls": True,
"user": "me", "password": "secret"}
)
For more control, you can use SMTPBackend
directly.
Reusing Connections¶
When you call send() with the same smtp dict
on the same message, the library automatically reuses the SMTP connection
through an internal pool. Connections with identical parameters share
a backend:
smtp_config = {"host": "smtp.example.com", "port": 587, "tls": True,
"user": "me", "password": "secret"}
# These two calls reuse the same underlying SMTP connection
message.send(to="alice@example.com", smtp=smtp_config)
message.send(to="bob@example.com", smtp=smtp_config)
For explicit connection management, create an SMTPBackend instance
and pass it instead of a dict. The backend supports context managers:
from emails.backend.smtp import SMTPBackend
with SMTPBackend(host="smtp.example.com", port=587,
tls=True, user="me", password="secret") as backend:
for recipient in recipients:
message.send(to=recipient, smtp=backend)
# Connection is closed automatically
SSL vs STARTTLS¶
The library supports two encryption modes:
Implicit SSL (
ssl=True): Connects over TLS from the start. Typically used with port 465.message.send(smtp={"host": "mail.example.com", "port": 465, "ssl": True, "user": "me", "password": "secret"})
STARTTLS (
tls=True): Connects in plain text, then upgrades to TLS. Typically used with port 587.message.send(smtp={"host": "smtp.example.com", "port": 587, "tls": True, "user": "me", "password": "secret"})
You cannot set both ssl and tls to True – this raises a
ValueError.
Timeouts¶
The default socket timeout is 5 seconds. You can change it with the
timeout parameter:
message.send(smtp={"host": "smtp.example.com", "timeout": 30})
Debugging¶
Enable SMTP protocol debugging to see the full conversation with the server on stdout:
message.send(smtp={"host": "smtp.example.com", "debug": 1})
All SMTP Parameters¶
The full list of parameters accepted in the smtp dict (or as
SMTPBackend constructor arguments):
host– SMTP server hostnameport– server port (int)ssl– use implicit SSL/TLS (for port 465)tls– use STARTTLS (for port 587)user– authentication usernamepassword– authentication passwordtimeout– socket timeout in seconds (default:5)debug– debug level (0= off,1= verbose)fail_silently– ifTrue(default), return errors in the response instead of raising exceptionslocal_hostname– FQDN for the EHLO/HELO command (auto-detected if not set)keyfile– path to SSL key filecertfile– path to SSL certificate filemail_options– list of ESMTP MAIL command options (e.g.,["smtputf8"])
HTML Transformations¶
The transform() method processes the HTML body
before sending – inlining CSS, loading images, removing unsafe tags,
and more.
message = emails.Message(
html="<style>h1{color:red}</style><h1>Hello!</h1>"
)
message.transform()
After transformation, the inline style is applied directly:
print(message.html)
# <html><head></head><body><h1 style="color:red">Hello!</h1></body></html>
Parameters¶
transform() accepts the following keyword arguments:
css_inline(default:True)Inline CSS styles using premailer. External stylesheets referenced in
<link>tags are loaded and converted to inlinestyleattributes.remove_unsafe_tags(default:True)Remove potentially dangerous HTML tags:
<script>,<object>,<iframe>,<frame>,<base>,<meta>,<link>,<style>.set_content_type_meta(default:True)Add a
<meta http-equiv="Content-Type">tag to the<head>with the message’s charset.load_images(default:True)Load images referenced in the HTML and embed them as message attachments. Accepts
True,False, or a callable for custom filtering (see below).images_inline(default:False)When
True, loaded images are embedded as inline attachments usingcid:references instead of regular attachments.
The following parameters are deprecated and have no effect:
make_links_absolutePremailer always makes links absolute. Passing
Falsetriggers aDeprecationWarning.update_stylesheetPremailer does not support this feature. Passing
Truetriggers aDeprecationWarning.
Custom Image Filtering¶
Pass a callable as load_images to control which images are loaded:
def should_load(element, hints=None, **kwargs):
# Skip tracking pixels
src = element.attrib.get("src", "")
if "track" in src or "pixel" in src:
return False
return True
message.transform(load_images=should_load)
You can also use the data-emails attribute in your HTML to control
individual images:
data-emails="ignore"– skip loading this imagedata-emails="inline"– load as an inline attachment
Custom Link and Image Transformations¶
For more specific transformations, access the transformer property
directly.
Transforming Image URLs¶
apply_to_images() applies a function
to all image references in the HTML – <img src>, background
attributes, and CSS url() values in style attributes:
message = emails.Message(html='<img src="promo.png">')
message.transformer.apply_to_images(
func=lambda src, **kw: "https://cdn.example.com/images/" + src
)
message.transformer.save()
print(message.html)
# <html><body><img src="https://cdn.example.com/images/promo.png"></body></html>
The callback receives uri (the current URL) and element (the lxml
element), and should return the new URL.
You can limit the scope with keyword arguments:
images=True– apply to<img src>(default:True)backgrounds=True– apply tobackgroundattributes (default:True)styles_uri=True– apply to CSSurl()in style attributes (default:True)
Transforming Link URLs¶
apply_to_links() applies a function
to all <a href> values:
message = emails.Message(html='<a href="/about">About</a>')
message.transformer.apply_to_links(
func=lambda href, **kw: "https://example.com" + href
)
message.transformer.save()
print(message.html)
# <html><body><a href="https://example.com/about">About</a></body></html>
Always call message.transformer.save() after using apply_to_images
or apply_to_links to update the message’s HTML body.
Making Images Inline Manually¶
You can mark individual attachments as inline and synchronize the HTML references:
message = emails.Message(html='<img src="promo.png">')
message.attach(filename="promo.png", data=open("promo.png", "rb"))
message.attachments["promo.png"].is_inline = True
message.transformer.synchronize_inline_images()
message.transformer.save()
print(message.html)
# <html><body><img src="cid:promo.png"></body></html>
Loaders¶
Loader functions create Message instances from various sources,
automatically handling HTML parsing, CSS inlining, and image embedding.
All loaders are in the emails.loader module.
Loading from a URL¶
from_url() fetches an HTML page and embeds all
referenced images and stylesheets:
import emails.loader
message = emails.loader.from_url(
url="https://example.com/newsletter/2024-01/index.html",
requests_params={"timeout": 30}
)
The requests_params dict is passed to the underlying HTTP requests
(for controlling timeouts, SSL verification, headers, etc.).
Loading from a ZIP Archive¶
from_zip() reads an HTML file and its resources
from a ZIP archive. The archive must contain at least one .html file:
message = emails.loader.from_zip(
open("template.zip", "rb"),
message_params={"subject": "Newsletter", "mail_from": "news@example.com"}
)
Loading from a Directory¶
from_directory() loads from a local directory.
It looks for index.html (or index.htm) automatically:
message = emails.loader.from_directory(
"/path/to/email-template/",
message_params={"subject": "Welcome", "mail_from": "hello@example.com"}
)
Loading from a File¶
from_file() loads from a single HTML file. Images
and CSS are resolved relative to the file’s directory:
message = emails.loader.from_file("/path/to/email-template/welcome.html")
Loading from an .eml File¶
from_rfc822() parses an RFC 822 email (e.g., a
.eml file). Set parse_headers=True to copy Subject, From, To,
and other headers:
message = emails.loader.from_rfc822(
open("archived.eml", "rb").read(),
parse_headers=True
)
This loader is primarily intended for demonstration and testing purposes.
When to Use Which Loader¶
from_html – you already have HTML as a string and want to process it (inline CSS, embed images)
from_url – the email template is hosted on a web server
from_directory – the template is a local folder with HTML, images, and CSS files
from_zip – the template is distributed as a ZIP archive
from_file – you have a single local HTML file
from_rfc822 – you want to re-create a message from an existing
.emlfile
Django Integration¶
python-emails provides DjangoMessage, a
Message subclass that sends through Django’s email backend.
from emails.django import DjangoMessage
message = DjangoMessage(
html="<p>Hello {{ name }}!</p>",
subject="Welcome",
mail_from="noreply@example.com"
)
result = message.send(to="user@example.com", context={"name": "Alice"})
Key differences from Message:
Uses
contextinstead ofrenderfor template variables.Uses Django’s configured email backend (
django.core.mail.get_connection()) instead of ansmtpdict.Returns
1on success and0on failure (matching Django’ssend_mailconvention).Accepts an optional
connectionparameter for a custom Django email backend connection.
Using a custom Django connection:
from django.core.mail import get_connection
from emails.django import DjangoMessage
message = DjangoMessage(
html="<p>Notification</p>",
subject="Alert",
mail_from="alerts@example.com"
)
connection = get_connection(backend="django.core.mail.backends.smtp.EmailBackend")
message.send(to="admin@example.com", connection=connection)
Django email settings (EMAIL_HOST, EMAIL_PORT, etc.) are used
automatically when no explicit connection is provided.
Flask Integration¶
For Flask applications, use the flask-emails extension, which provides Flask-specific integration (app factory support, configuration from Flask config, etc.):
from flask_emails import Message
message = Message(
html="<p>Hello!</p>",
subject="Test",
mail_from="sender@example.com"
)
message.send(to="user@example.com")
Install with:
pip install flask-emails
Refer to the flask-emails documentation for configuration details.
Charset and Encoding¶
python-emails uses two separate encoding settings:
charset– encoding for the message body (default:'utf-8')headers_encoding– encoding for email headers (default:'ascii')
Changing the Body Charset¶
For messages in specific encodings (e.g., Cyrillic), set the charset
parameter:
message = emails.html(
html="<p>Content in specific encoding</p>",
charset="windows-1251",
mail_from="sender@example.com"
)
The library automatically registers proper encoding behaviors for common
charsets including utf-8, windows-1251, and koi8-r.
Internationalized Domain Names (IDN)¶
Email addresses with internationalized domain names work with the standard address format. The library handles encoding automatically:
message = emails.html(
html="<p>Hello!</p>",
mail_from=("Sender", "user@example.com"),
mail_to=("Recipient", "user@example.com")
)
Headers¶
Custom Headers¶
Pass a headers dict when creating a message to add custom email
headers:
message = emails.html(
html="<p>Hello!</p>",
subject="Test",
mail_from="sender@example.com",
headers={
"X-Mailer": "python-emails",
"X-Priority": "1",
"List-Unsubscribe": "<mailto:unsubscribe@example.com>"
}
)
Non-ASCII characters in header values are automatically encoded according to RFC 2047.
Header values are validated – newline characters (\n, \r)
raise BadHeaderError to prevent header injection attacks.