Writing Mails from Rust (2/3): The mail crate
Introduction to the mail crate, a rust library for creating mails
The mail crate is a modular Rust library for
creating, modifying and then encoding mails. It also has bindings to our new
crate new-tokio-smtp to allow
sending mails asynchronously, as well as bindings to handlebars for creating
mails from templates. It currently does not support parsing mails, but is
designed in a way that decoding capabilities could be easily added in the
future (contact me if that is something you'd be interested in working on!). At
1aim, we are already using mail in production.
The actual mail crate itself is just a facade for a number of underlying
crates like mail-core,
mail-headers or
mail-smtp (i.e. it
re-exports the functionality of these crates). Other projects are welcome to
use some of the sub-crates in their own mail-related projects.
The core element of the mail crate is the Mail
type, which
consists of a number of type safe mail headers and a mail body. As already
mentioned in the previous post mail bodies
can either contain some data directly or a number of mail bodies. As each of
the bodies has their own headers it's mostly the same as having a mail inside
of a mail (you can actually do that). So the Mail types body is either a
Resource representing some data (as well as metadata) or a vector of mails.
After creating a Mail object it is still possible to modify it, compose it
with other Mail instances to create a new Mail object or encode it into a
byte buffer/string to create an actual mail. There is a number of restrictions
to mails, e.g., they have to have a Date header and if there are multiple
mailboxes in the From header there has to be a Sender header. The mail
crate validates these constraints only before encoding (when turning a Mail
into a EncodableMail). This means that the Mail type instances do
intentionally not check these restrictions as this makes it much easier to work
with them. E.g. you can create a mail from a template in some function and then
pass it to another function which adds From/To and then to another which
adds some special headers. This makes it very easy to write reusable functions
for parts of the mail creation process and keeps concerns like creating a mail
content from a template and adding special headers cleanly separated. Due to
the type safe nature of the mail headers it is furthermore possible to access
headers in a Mail instance to e.g. get all mail addresses in a From/To
header or the Sender headers mailbox to use them as sender/recipient in SMTP.
Below is a general overview of features supported by the mail create:
Mailtype which is a structured representation of a mail (instead of just some string concatenations).- Type safe headers.
- Allows creating custom headers.
- Line length limits and line breaking is handled by the
EncodingBufferfor all mails. - Header data representation is done through "header components" which allow
reusing of data representation and encoding functions between headers (e.g.
the
FromandToheader are represented exactly the same, as a list of mailboxes, which are also reused in other headers, e.g.Reply-To). - Handles the fact that most but not all headers can only appear one time in a header section of a mail and for most but not all headers the order in which they are in a header section is irrelevant.
- Differentiates between classical mails and internationalised mails when encoding a mail to appropriately handle non-US-ASCII UTF-8 text in headers.
- Auto-generates important headers like
DateorMessage-Id- This also includes other aspects like automatically populating
Content-Dispositionheaders with parameters like e.g.file-metaif the metadata is available but not yet in the header, making it much easier to create mails with attachments correctly. - The crate makes sure
Content-IdandMessage-Idare world unique (if setup correctly, if not they are still most likely world unique which is much better than what some other frameworks give you).
- This also includes other aspects like automatically populating
- Makes sure the mail is valid
- Makes sure there is a
DateandFromheader. - If there are multiple mailboxes in
Frommake sure there is aSenderheader. - etc.
- Makes sure there is a
- Allows easy handling of "resources" like images, HTML bodies and attachments.
- Resources can be passed in "just" as data, with some metadata like their content type.
- But resources can also be passed in through an
IRIand "loaded" when the mail is prepared for encoding. This allows to e.g. refer to your companies logo always aspath:./images/logo.pngas well as cache the loading and transfer encoding of it (or just directly store abase64encoded image on disk if you want to). - Async loading of resources is supported, allowing a better incorporation
with a async stack or e.g.
new-tokio-smtp.- If you don't use async you can just use
Future::waitand it will just work out.
- If you don't use async you can just use
- While the resource handling over
IRIis extremely flexible this normally comes at a price. Because of this it is made in a way that you don't have to use it.- You still have to create and use a
Contextinstance, but this is needed anyway for world unique IDs forMessage-Id/Content-Idso you can just use the providedsimple_contextimplementation.
- You still have to create and use a
- Provides bindings to
new-tokio-smtpto "simply" send mails created with themailcrate to anMSA(e.g.,smtp.example-mail-provider.com).- E.g.
Connection::sendwill send aMailover the givenSMTPconnection. - There are other methods for sending batches of mails or doing things like opening a connection, sending the batch of mails and then closing the connection again.
- The
SMTPbindings will get theSMTPsender/recipients from theFrom/To/Senderheaders if no sender/recipients where explicitly passed in.
- E.g.
- Provides an easy way to create "common" mails with the
MailPartstype- Allows the library user to not need to think about which multipart bodies to use in which way when adding alternate bodies, embedded content or attachments.
MailPartsdefine which alternate bodies with which embedded content and which attachments are to be included in the mail and then can be turned into aMailinstance having an appropriate multipart body tree.
- Provides functionality for creating bindings to a template engine
- When binding to a typical "text" template engine there are many parts which
are common for most template engines, the provided functionality allows to
not have to rewrite this every time
- E.g. having one "text" template per alternate body.
- Having a potential additional template for the
Subjectheader. - Having a way to specify common embedded content and attachments.
- Having a way to pass in additional embedded content and attachments with
the template data.
- E.g. a users avatar image.
- Existing bindings for
handlebarstemplates. - Allows specifying templates as a
tomlfile referring to template files and content files (e.g. images) in the same folder with relative path.- But actually depends mainly on
serdeso having template specifications in other formats like e.g.JSONis also possible.
- But actually depends mainly on
- When binding to a typical "text" template engine there are many parts which
are common for most template engines, the provided functionality allows to
not have to rewrite this every time
- And other smaller things ;=)
In the next part (coming soon) of this blog post I will go through how to setup
a Context, a handlebars mail template then create a mail from it and send it
over SMTP covering the "basic" needs of most applications for sending mails. It
will also cover why some of the design is the way it is.