- fully typed
- full utf-8 support
- async and sync sending
- pluggable transports
- multiple built-in transports including: SMTP, file, null, in-memory, streaming, and console.
- message preprocessors
- embeddables
- attachments (with async and sync interfaces)
- message signing via Signer interface (DKIM bundled)
- message encryption via Encrypter interface
- trio support via anyio (the library itself, some backends may not be compatible)
- fallback transports
- global From address
- templated emails
pip install mailers[smtp]
Then create mailer:
from mailers import Mailer
mailer = Mailer("smtp://user:password@localhost:25?timeout=2")
await mailer.send_message(
to="user@localhost", from_address="from@localhost", subject="Hello", text="World!"
)
If you need more control over the message, you can use Email
object to construct email message and then send it
using mailer.send
method.
from mailers import Email, Mailer
message = Email(
to="user@localhost",
from_address="from@example.tld",
cc="cc@example.com",
bcc=["bcc@example.com"],
text="Hello world!",
html="<b>Hello world!</b>",
)
mailer = Mailer("smtp://")
await mailer.send(message)
Instead of setting "From" header in every message, you can set it mailer-wide. Use from_address
argument of Mailer
class:
mailer = Mailer(from_address="sender@localhost")
The mailer will set From header with the given value to all messages that do not container From or Sender headers.
Requires
jinja2
package installed
You can use Jinja to render templates. This way, your text
and html
can be rendered from a template.
Use TemplatedMailer
instead of default Mailer
and set a jinja2.Environment
instance.
Then, call send_templated_message
.
import jinja2
from mailers import TemplatedMailer
env = jinja2.Environment(loader=jinja2.FileSystemLoader(["templates"]))
mailer = TemplatedMailer("smtp://", env)
mailer.send_templated_message(
to="...",
subject="Hello",
text_template="mail.txt",
html_template="mail.html",
template_context={"user": "root"},
)
Use attach
, attach_from_path
, attach_from_path_sync
methods to attach files.
from mailers import Email
message = Email(
to="user@localhost", from_address="from@example.tld", text="Hello world!"
)
# attachments can be added on demand
await message.attach_from_path("file.txt")
# or use blocking sync version
message.attach_from_path_sync("file.txt")
# attach from variable
message.attach("CONTENTS", "file.txt", "text/plain")
In the same way as with attachments, you can inline file into your messages. This is commonly used to display embedded
images in the HTML body. Here are method you can use embed
, embed_from_path
, embed_from_path_sync
.
from mailers import Email
message = Email(
to="user@localhost",
from_address="from@example.tld",
html='Render me <img src="cid:img1">',
)
await message.embed_from_path(path="/path/to/image.png", name="img1")
Note, that you have to add HTML part to embed files. Otherwise, they will be ignored.
You can sign messages (e.g. with DKIM) by passing signer
argument to the Mailer
instance.
signer = MySigner()
mailer = Mailer(..., signer=signer)
Requires
dkimpy
package installed
You may wish to add DKIM signature to your messages to prevent them from being put into the spam folder.
Note, you need to install dkimpy
package before using this feature.
from mailers import Mailer
from mailers.signers.dkim import DKIMSigner
signer = DKIMSigner(selector="default", private_key_path="/path/to/key.pem")
# or you can put key content using private_key argument
signer = DKIMSigner(selector="default", private_key="PRIVATE KEY GOES here...")
mailer = Mailer("smtp://", signer=signer)
Now all outgoing messages will be signed with DKIM method.
The plugin signs "From", "To", "Subject" headers by default. Use "headers" argument to override it.
Extend mailers.Signer
class and implement sign
method:
from email.message import Message
from mailers import Signer
class MySigner(Signer):
def sign(self, message: Message) -> Message:
# message signing code here...
return message
When encrypting a message, the entire message (including attachments) is encrypted using a certificate. Therefore, only the recipients that have the corresponding private key can read the original message contents.
encrypter = MyEncrypter()
mailer = Mailer(..., encrypter=encrypter)
Now all message content will be encrypted.
Extend mailers.Encrypter
class and implement encrypt
method:
from email.message import Message
from mailers import Encrypter
class MyEncrypter(Encrypter):
def encrypt(self, message: Message) -> Message:
# message encrypting code here...
return message
Use MultiTransport
to provide a fallback transport. By default, the first transport is used but if it fails to send
the message, it will retry sending using next configured transport.
from mailers import Mailer, MultiTransport, SMTPTransport
primary_transport = SMTPTransport()
fallback_transport = SMTPTransport()
mailer = Mailer(MultiTransport([primary_transport, fallback_transport]))
Preprocessors are function that mailer calls before sending. Preprocessors are simple functions that modify message contents.
Below you see an example preprocessor:
from email.message import EmailMessage
from mailers import Mailer
def attach_html_preprocessor(message: EmailMessage) -> EmailMessage:
message.add_alternative(b"This is HTML body", subtype="html", charset="utf-8")
return message
mailer = Mailer(preprocessors=[attach_html_preprocessor])
Requires
toronado
package installed
Out of the box we provide mailers.preprocessors.css_inliner
utility that converts CSS classes into inline styles.
Requires
aiosmtplib
package installed
Send messages via third-party SMTP servers.
Class: mailers.transports.SMTPTransport
directory smtp://user:pass@hostname:port?timeout=&use_tls=1
Options:
host
(string, default "localhost") - SMTP server hostport
(string, default "25") - SMTP server portuser
(string) - SMTP server loginpassword
(string) - SMTP server login passworduse_tls
(string, choices: "yes", "1", "on", "true") - use TLStimeout
(int) - connection timeoutcert_file
(string) - path to certificate filekey_file
(string) - path to key file
Write outgoing messages into a directory in EML format.
Class: mailers.transports.FileTransport
DSN: file:///tmp/mails
Options:
directory
(string) path to a directory
Discards outgoing messages. Takes no action on send.
Class: mailers.transports.NullTransport
DSN: null://
Keeps all outgoing messages in memory. Good for testing.
Class: mailers.transports.InMemoryTransport
DSN: memory://
Options:
storage
(list of strings) - outgoing message container
You can access the mailbox via ".mailbox" attribute.
Example:
from mailers import Mailer, InMemoryTransport, Email
transport = InMemoryTransport([])
mailer = Mailer(transport)
await mailer.send(Email(...))
assert len(transport.mailbox) == 1 # here are all outgoing messages
Writes all messages into a writable stream. Ok for local development.
Class: mailers.transports.StreamTransport
DSN: unsupported
Options:
output
(typing.IO) - a writable stream
Example:
import io
from mailers import Mailer, StreamTransport
transport = StreamTransport(output=io.StringIO())
mailer = Mailer(transport)
This is a preconfigured subclass of streaming transport. Writes to sys.stderr
by default.
Class: mailers.transports.ConsoleTransport
DSN: console://
Options:
output
(typing.IO) - a writeable stream
The purpose of this transport is to provide a developer an option to provide a fallback transport.
You can configure several channels and MultiTransport
will guarantee that at least one will deliver the message.
Class: mailers.transports.MultiTransport
DSN: -
Options:
transports
(list[Transport]) - subtransports
Each transport must extend mailers.transports.Transport
base class.
from email.message import Message
from mailers import Mailer, Transport
class PrintTransport(Transport):
async def send(self, message: Message) -> None:
print(str(message))
mailer = Mailer(PrintTransport())