eletter — Simple e-mail composition & decomposition¶
GitHub | PyPI | Documentation | Issues | Changelog
Tutorial¶
Basic Composition¶
eletter
can be used to construct a basic text e-mail using the compose()
function like so:
from eletter import compose
msg = compose(
subject="The subject of the e-mail",
from_="sender@domain.com",
to=["recipient@domain.com", "another.recipient@example.nil"],
text="This is the body of the e-mail. Write what you want here!\n",
)
Note
Observe that the from_
argument is spelled with an underscore. It has
to be this way, because plain old from
is a keyword in Python.
If you want to construct an HTML e-mail, use the html
keyword instead of
text
:
from eletter import compose
msg = compose(
subject="The subject of the e-mail",
from_="sender@domain.com",
to=["recipient@domain.com", "another.recipient@example.nil"],
html=(
"<p>This is the <strong>body</strong> of the <em>e</em>-mail."
" <span style='color: red;'>Write what you want here!</span></p>\n"
),
)
By specifying both text
and html
, you’ll get an e-mail whose HTML part
is displayed if the e-mail reader supports it and whose text part is displayed
instead on lower-tech clients.
from eletter import compose
msg = compose(
subject="The subject of the e-mail",
from_="sender@domain.com",
to=["recipient@domain.com", "another.recipient@example.nil"],
text="This is displayed on plain text clients.\n",
html="<p>This is displayed on graphical clients.<p>\n",
)
Addresses¶
In the examples so far, e-mail addresses have just been specified as, well,
addresses. However, addresses usually belong to people or organizations with
names; we can include these names alongside the addresses by constructing
Address
objects from pairs of “display names” and e-mail addresses:
from eletter import Address, compose
msg = compose(
subject="The subject of the e-mail",
from_=Address("Sender's name goes here", "sender@domain.com"),
to=[
Address("Joe Q. Recipient", "recipient@domain.com"),
Address("Jane Z. Another-Recipient", "another.recipient@example.nil"),
],
text="This is the body of the e-mail. Write what you want here!\n",
)
Sometimes addresses come in named groups. We can represent these with the
Group
class, which takes a name for the group and an iterable of address
strings and/or Address
objects:
from eletter import Address, Group, compose
msg = compose(
subject="The subject of the e-mail",
from_="sender@domain.com",
to=[
Group(
"friends",
[
Address("Joe Q. Recipient", "recipient@domain.com"),
Address("Jane Z. Another-Recipient", "another.recipient@example.nil"),
"anonymous@nowhere.nil",
],
),
Address("Mr. Not-in-a-Group", "ungrouped@unkno.wn"),
Group(
"enemies",
[
"that.guy@over.there",
"knows.what.they.did@ancient.history",
Address("Anemones", "sea.flora@ocean.net"),
],
),
],
text="This is the body of the e-mail. Write what you want here!\n",
)
CC, BCC, etc.¶
Besides From and To addresses, compose()
also
accepts optional arguments for CC, BCC,
Reply-To, and Sender addresses:
from eletter import Address, compose
msg = compose(
from_=Address("Mme E.", "me@here.com"),
to=["you@there.net", Address("Thaddeus Hem", "them@hither.yon")],
cc=[Address("Cee Cee Cecil", "ccc@seesaws.cc"), "coco@nu.tz"],
bcc=[
"eletter@depository.nil",
Address("Secret Cabal", "illuminati@new.world.order"),
"mom@house.home",
],
reply_to="replyee@some.where",
sender="steven.ender@big.senders",
subject="To: Everyone",
text="Meeting tonight! You know the place. Bring pizza.\n",
)
Note
The to
, cc
, and bcc
arguments always take lists or iterables of
addresses. from_
and reply_to
, on the other hand, can be set to
either a single address or an iterable of addresses. sender
must
always be a single address.
Attachments¶
Attachments come in two common types: text and binary. eletter
provides a
class for each, TextAttachment
and BytesAttachment
.
We can construct a BytesAttachment
as follows:
from eletter import BytesAttachment
attachment = BytesAttachment(
b'... binary data goes here ...',
filename="name-of-attachment.dat"
)
This will create an application/octet-stream attachment with an
“attachment” disposition (meaning that most clients will just display it as a
clickable icon). To set the content type to something more informative, set
the content_type
parameter to the relevant MIME type. To have the
attachment displayed inline (generally only an option for images & videos), set
the inline
parameter to true. Hence:
from eletter import BytesAttachment
attachment = BytesAttachment(
b'... binary data goes here ...',
filename="name-of-attachment.png"
content_type="image/png",
inline=True,
)
If your desired attachment exists as a file on your system, you can construct a BytesAttachment
from the file directly with the from_file()
classmethod:
from eletter import BytesAttachment
attachment = BytesAttachment.from_file(
"path/to/file.dat",
content_type="application/x-custom",
inline=True,
)
The basename of the given file will be used as the filename of the attachment.
(If you want to use a different name, set the filename
attribute on the attachment after creating it.) If content_type
is not
given, the MIME type of the file will be guessed based on its file extension.
The TextAttachment
class is analogous to BytesAttachment
, except that it is
constructed from a str
instead of bytes
, and the content_type
(which
defaults to "text/plain"
) must be a text type.
Once you’ve created some attachment objects, they can be attached to an e-mail
by passing them in a list as the attachments
argument:
from eletter import BytesAttachment, TextAttachment, compose
spreadsheet = TextAttachment.from_file("income.csv")
image = BytesAttachment.from_file("cat.jpg")
msg = compose(
subject="That data you wanted",
from_="sender@domain.com",
to=["recipient@domain.com"],
text="Here's that data you wanted, sir. And also the ... other thing.\n",
attachments=[spreadsheet, image],
)
Attaching E-mails to E-mails¶
On rare occasions, you may have an e-mail that you want to completely embed in
a new e-mail as an attachment. With eletter
, you can do this with the
EmailAttachment
class. It works the same as BytesAttachment
and
TextAttachment
, except that the content must be an
email.message.EmailMessage
instance, and you can’t set the
Content-Type (which is always message/rfc822). Like
the other attachment classes, EmailAttachment
also has a
from_file()
classmethod for constructing an instance from an
e-mail in a file.
Date and Extra Headers¶
compose()
takes two more parameters that we haven’t mentioned yet. First is
date
, which lets you set the Date header in an e-mail to a
given datetime.datetime
instance. Second is headers
, which lets you set
arbitrary extra headers on an e-mail by passing in a dict
. Each value in the
dict
must be either a string or (if you want to set multiple headers with the
same name) an iterable of strings.
from datetime import datetime
from eletter import compose
msg = compose(
subject="The subject of the e-mail",
from_="sender@domain.com",
to=["recipient@domain.com", "another.recipient@example.nil"],
text="This is the body of the e-mail. Write what you want here!\n",
date=datetime(2021, 3, 10, 17, 56, 36).astimezone(),
headers={
"User-Agent": "My Mail Application v.1",
"Priority": "urgent",
"Comments": [
"I like e-mail.",
"But no one ever looks at e-mails' sources, so no one will ever know.",
]
},
)
multipart/mixed Messages¶
All the e-mails constructed so far, when viewed in an e-mail client, have their
attachments listed at the bottom. What if we want to mix & match attachments
and text, switching from text to an attachment and then back to text?
eletter
lets you do this by providing TextBody
and HTMLBody
classes
that can be &
-ed with attachments to produce multipart/mixed
messages, like so:
from eletter import BytesAttachment, TextBody
part1 = TextBody("Look at the pretty kitty!\n")
snuffles = BytesAttachment.from_file("snuffles.jpeg", inline=True)
part2 = TextBody("Now look at this dog.\n")
rags = BytesAttachment.from_file("rags.jpeg", inline=True)
part3 = TextBody("Which one is cuter?\n")
mixed = part1 & snuffles & part2 & rags & part3
We can then convert mixed
into an EmailMessage
by calling
its compose()
method, which takes the same arguments as the
compose()
function, minus text
, html
, and attachments
.
msg = mixed.compose(
subject="The subject of the e-mail",
from_="sender@domain.com",
to=["recipient@domain.com", "another.recipient@example.nil"],
)
When the resulting e-mail is viewed in a client, you’ll see three lines of text with images between them.
Tip
As a shortcut, you can combine a bare str
with an eletter
object
using |
or the other overloaded operators described below (&
and
^
), and that str
will be automatically converted to a TextBody
.
The example above could thus be rewritten:
from eletter import BytesAttachment, TextBody
snuffles = BytesAttachment.from_file("snuffles.jpeg", inline=True)
rags = BytesAttachment.from_file("rags.jpeg", inline=True)
mixed = (
"Look at the pretty kitty!\n"
& snuffles
& "Now look at this dog.\n"
& rags
& "Which one is cuter?\n"
)
multipart/alternative Messages¶
Now that we know how to construct mixed messages, how do we use them to create
messages with both mixed-HTML and mixed-text payloads where the client shows
whichever mixed payload it can support? The answer is the |
operator;
using it to combine two eletter
objects will give you a
multipart/alternative object, representing an e-mail message with
two different versions of the same content that the client will then pick
between.
from eletter import BytesAttachment, HTMLBody, TextBody
text1 = TextBody("Look at the pretty kitty!\n")
text2 = TextBody("Now look at this dog.\n")
text3 = TextBody("Which one is cuter?\n")
html1 = HTMLBody("<p>Look at the <em>pretty kitty</em>!</p>\n")
html2 = HTMLBody("<p>Now look at this <strong>dog</strong>.</p>\n")
html3 = HTMLBody("<p>Which one is <span style='color: pink'>cuter</span>?</p>\n")
snuffles = BytesAttachment.from_file("snuffles.jpeg", inline=True)
rags = BytesAttachment.from_file("rags.jpeg", inline=True)
mixed_text = text1 & snuffles & text2 & rags & text3
mixed_html = html1 & snuffles & html2 & rags & html3
alternative = mixed_text | mixed_html
The alternative
object can then be converted to an e-mail with the same
compose()
method that mixed objects have.
Tip
In this specific example, we can save on e-mail size by instead creating a mixed message containing alternative parts, like so:
mixed = (text1 | html1) & snuffles & (text2 | html2) & rags & (text3 | html3)
Tip
The parts of a multipart/alternative message should generally
be placed in increasing order of preference, which means that the text part
should be on the left of the |
and the HTML part should be on the
right.
Sending E-mails¶
Once you’ve constructed your e-mail and turned it into an
EmailMessage
object, you can send it using Python’s smtplib
,
imaplib
, or mailbox
modules or using a compatible third-party library.
Actually doing this is beyond the scope of this tutorial & library, but may I
suggest outgoing, by yours truly?
Decomposing Emails¶
New in version 0.5.0.
If you have an email.message.EmailMessage
instance (either composed using
eletter
or acquired elsewhere) and you want to convert it into an
eletter
structure to make it easier to work with, eletter
provides a
decompose()
function for doing just that. Calling decompose()
on an
EmailMessage
produces an Eletter
instance that has
attributes for all of the fields accepted by the compose()
method
plus a content
field containing an eletter
class.
Tip
If you want to decompose a message that is a plain email.message.Message
instance but not an EmailMessage
instance, you need to
first convert it into an EmailMessage
before passing it to
decompose()
or decompose_simplify()
. This can be done with the
message2email()
function from the mailbits package.
If you want to decompose a message even further, you can call the
decompose_simple()
function on an EmailMessage
or call the
simplify()
method of an Eletter
to produce a SimpleEletter
instance. In place of a content
attribute, a SimpleEletter
has
text
, html
, and attachments
attributes giving the original message’s text and HTML bodies plus any
attachments.
Once you’ve decomposed and/or simplified a message, you can examine its parts
and do whatever you want with that information. You can also manually modify
the Eletter
/SimpleEletter
’s various attributes and then call its
compose()
method (which takes no arguments) to recompose the
instance into a modified EmailMessage
. Note that the
attributes are of stricter types than what is accepted by the corresponding
arguments to the compose()
function. In particular, addresses must be
specified as Address
instances, not as strings 1, the from_
and
reply_to
attributes must always be lists, and the values of the
headers
attribute must always be lists.
Note
Most EmailMessage
instances can be decomposed into
Eletter
instances; those that can’t use Content-Types not
supported by eletter
, i.e., message types other than
message/rfc822 or multipart types other than
multipart/alternative, multipart/mixed, and
multipart/related.
On the other hand, considerably fewer EmailMessage
instances can be simplified into SimpleEletter
instances. Messages that
cannot be simplified include messages without plain text or HTML parts,
mixed messages that alternate between plain text & HTML without supplying
both types for every body part, multipart/related messages with
more than one part, multipart/mixed messages containing
multipart/alternative parts that do not consist of a plain text
body plus an HTML body, and other unusual things. Trying to simplify such
messages will produce SimplificationError
s.
One category of messages can be simplified, but not without loss of
information, and so they are not simplified by default: namely,
multipart/mixed messages that alternate between bodies and
attachments rather than placing all attachments at the end of the message.
By default, trying to simplify such a message produces a
MixedContentError
; however, if the unmix
argument to
decompose_simple()
or Eletter.simplify()
is set to True
, such
messages will instead be simplified by separating the attachments from the
bodies, which are then concatenated with no indication of where the
attachments were located in the text.
API¶
The compose()
Function¶
-
eletter.
compose
(*, to: Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]], from_: Optional[Union[str, email.headerregistry.Address, email.headerregistry.Group, Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]]] = None, subject: Optional[str] = None, text: Optional[str] = None, html: Optional[str] = None, cc: Optional[Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]] = None, bcc: Optional[Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]] = None, reply_to: Optional[Union[str, email.headerregistry.Address, email.headerregistry.Group, Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]]] = None, sender: Optional[Union[str, email.headerregistry.Address]] = None, date: Optional[datetime.datetime] = None, headers: Optional[Mapping[str, Union[str, Iterable[str]]]] = None, attachments: Optional[Iterable[eletter.classes.Attachment]] = None) → email.message.EmailMessage[source]¶ Construct an
EmailMessage
instance from a subject, From address, To addresses, and a plain text and/or HTML body, optionally accompanied by attachments and other headers.All parameters other than
to
and at least one oftext
andhtml
are optional.Changed in version 0.2.0:
from_
andreply_to
may now be passed lists of addresses.Changed in version 0.4.0:
from_
may now beNone
or omitted.Changed in version 0.4.0: All arguments are now keyword-only.
Changed in version 0.5.0:
subject
may now beNone
or omitted.- Parameters
subject (str) – The e-mail’s Subject line
to (iterable of addresses) – The e-mail’s To line
from_ (address or iterable of addresses) – The e-mail’s From line. Note that this argument is spelled with an underscore, as “
from
” is a keyword in Python.text (str) – The contents of a text/plain body for the e-mail. At least one of
text
andhtml
must be specified.html (str) – The contents of a text/html body for the e-mail. At least one of
text
andhtml
must be specified.cc (iterable of addresses) – The e-mail’s CC line
bcc (iterable of addresses) – The e-mail’s BCC line
reply_to (address or iterable of addresses) – The e-mail’s Reply-To line
sender (address) – The e-mail’s Sender line. The address must be a string or
Address
, not aGroup
.date (datetime) – The e-mail’s Date line
attachments (iterable of attachments) – A collection of attachments to append to the e-mail
headers (mapping) – A collection of additional headers to add to the e-mail. A header value may be either a single string or an iterable of strings to add multiple headers with the same name. If you wish to set an otherwise-unsupported address header like Resent-From to a list of addresses, use the
format_addresses()
function to first convert the addresses to a string.
- Return type
- Raises
ValueError – if neither
text
norhtml
is set
Addresses¶
Addresses in eletter
can be specified in three ways:
As an
"address@domain.com"
string giving just a bare e-mail addressAs an
eletter.Address("Display Name", "address@domain.com")
instance pairing a person’s name with an e-mail addressAs an
eletter.Group("Group Name", iterable_of_addresses)
instance specifying a group of addresses (strings orAddress
instances)
Note
eletter.Address
and eletter.Group
are actually just subclasses of
Address
and Group
from
email.headerregistry
with slightly more convenient constructors. You can
also use the standard library types directly, if you want to.
-
class
eletter.
Address
(display_name: str, address: str)[source]¶ A combination of a person’s name and their e-mail address
-
class
eletter.
Group
(display_name: str, addresses: Iterable[Union[str, email.headerregistry.Address]])[source]¶ New in version 0.2.0.
An e-mail address group
MailItem
Classes¶
-
class
eletter.
MailItem
[source]¶ New in version 0.3.0.
Base class for all
eletter
message components-
compose
(*, to: Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]], from_: Optional[Union[str, email.headerregistry.Address, email.headerregistry.Group, Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]]] = None, subject: Optional[str] = None, cc: Optional[Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]] = None, bcc: Optional[Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]] = None, reply_to: Optional[Union[str, email.headerregistry.Address, email.headerregistry.Group, Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]]]] = None, sender: Optional[Union[str, email.headerregistry.Address]] = None, date: Optional[datetime.datetime] = None, headers: Optional[Mapping[str, Union[str, Iterable[str]]]] = None) → email.message.EmailMessage[source]¶ Convert the
MailItem
into anEmailMessage
with the item’s contents as the payload and with the given subject, From address, To addresses, and optional other headers.All parameters other than
to
are optional.Changed in version 0.4.0:
from_
may now beNone
or omitted.Changed in version 0.4.0: All arguments are now keyword-only.
Changed in version 0.5.0:
subject
may now beNone
or omitted.- Parameters
subject (str) – The e-mail’s Subject line
to (iterable of addresses) – The e-mail’s To line
from_ (address or iterable of addresses) – The e-mail’s From line. Note that this argument is spelled with an underscore, as “
from
” is a keyword in Python.cc (iterable of addresses) – The e-mail’s CC line
bcc (iterable of addresses) – The e-mail’s BCC line
reply_to (address or iterable of addresses) – The e-mail’s Reply-To line
sender (address) – The e-mail’s Sender line. The address must be a string or
Address
, not aGroup
.date (datetime) – The e-mail’s Date line
headers (mapping) – A collection of additional headers to add to the e-mail. A header value may be either a single string or an iterable of strings to add multiple headers with the same name. If you wish to set an otherwise-unsupported address header like Resent-From to a list of addresses, use the
format_addresses()
function to first convert the addresses to a string.
- Return type
-
Attachments¶
-
class
eletter.
BytesAttachment
(content: bytes, filename: Optional[str], *, content_id: Optional[str] = None, content_type: str = NOTHING, inline: bool = False)[source]¶ A binary e-mail attachment.
content_type
defaults to"application/octet-stream"
.-
filename
: Optional[str]¶ The filename of the attachment
Changed in version 0.5.0:
filename
can now beNone
.
-
classmethod
from_file
(path: Union[bytes, str, os.PathLike[bytes], os.PathLike[str]], content_type: Optional[str] = None, inline: bool = False, content_id: Optional[str] = None) → BytesAttachment[source]¶ New in version 0.2.0.
Construct a
BytesAttachment
from the contents of the file atpath
. The filename of the attachment will be set to the basename ofpath
. Ifcontent_type
isNone
, the Content-Type is guessed based onpath
’s file extension.Changed in version 0.3.0:
inline
andcontent_id
arguments added
-
-
class
eletter.
EmailAttachment
(content: email.message.EmailMessage, filename: Optional[str], *, content_id: Optional[str] = None, inline: bool = False)[source]¶ New in version 0.2.0.
A message/rfc822 e-mail attachment
-
content
: email.message.EmailMessage¶ The body of the attachment
-
filename
: Optional[str]¶ The filename of the attachment
Changed in version 0.5.0:
filename
can now beNone
.
-
classmethod
from_file
(path: Union[bytes, str, os.PathLike[bytes], os.PathLike[str]], inline: bool = False, content_id: Optional[str] = None) → EmailAttachment[source]¶ Construct an
EmailAttachment
from the contents of the file atpath
. The filename of the attachment will be set to the basename ofpath
.
-
-
class
eletter.
TextAttachment
(content: str, filename: Optional[str], *, content_id: Optional[str] = None, content_type: str = NOTHING, inline: bool = False)[source]¶ A textual e-mail attachment.
content_type
defaults to"text/plain"
and must have a maintype of text.-
filename
: Optional[str]¶ The filename of the attachment
Changed in version 0.5.0:
filename
can now beNone
.
-
classmethod
from_file
(path: Union[bytes, str, os.PathLike[bytes], os.PathLike[str]], content_type: Optional[str] = None, encoding: Optional[str] = None, errors: Optional[str] = None, inline: bool = False, content_id: Optional[str] = None) → TextAttachment[source]¶ New in version 0.2.0.
Construct a
TextAttachment
from the contents of the file atpath
. The filename of the attachment will be set to the basename ofpath
. Ifcontent_type
isNone
, the Content-Type is guessed based onpath
’s file extension.encoding
anderrors
are used when opening the file and have no relation to the Content-Type.Changed in version 0.3.0:
inline
andcontent_id
arguments added
-
Body Classes¶
Multipart Classes¶
-
class
eletter.
Multipart
[source]¶ New in version 0.3.0.
Base class for all multipart classes. All such classes are mutable sequences of
MailItem
s supporting the usual methods (construction from an iterable, subscription,append()
,pop()
, etc.).
-
class
eletter.
Alternative
(content=NOTHING, *, content_id: Optional[str] = None)[source]¶ New in version 0.3.0.
A multipart/alternative e-mail payload. E-mails clients will display the resulting payload by choosing whichever part they support best.
An
Alternative
instance can be created by combining two or moreMailItem
s with the|
operator:text = TextBody("This is displayed on plain text clients.\n") html = HTMLBody("<p>This is displayed on graphical clients.<p>\n") alternative = text | html
Likewise, additional
MailItem
s can be added to anAlternative
instance with the|=
operator:# Same as above: alternative = Alternative() alternative |= TextBody("This is displayed on plain text clients.\n") alternative |= HTMLBody("<p>This is displayed on graphical clients.<p>\n")
Using
|
to combine aMailItem
with astr
automatically converts thestr
to aTextBody
:# Same as above: text = "This is displayed on plain text clients.\n" html = HTMLBody("<p>This is displayed on graphical clients.<p>\n") alternative = text | html assert alternative.contents == [ TextBody("This is displayed on plain text clients.\n"), HTMLBody("<p>This is displayed on graphical clients.<p>\n"), ]
When combining two
Alternative
instances with|
or|=
, the contents are “flattened”:# Same as above: txtalt = Alternative([ TextBody("This is displayed on plain text clients.\n") ]) htmlalt = Alternative([ HTMLBody("<p>This is displayed on graphical clients.<p>\n") ]) alternative = txtalt | htmlalt assert alternative.contents == [ TextBody("This is displayed on plain text clients.\n"), HTMLBody("<p>This is displayed on graphical clients.<p>\n"), ]
-
class
eletter.
Mixed
(content=NOTHING, *, content_id: Optional[str] = None)[source]¶ New in version 0.3.0.
A multipart/mixed e-mail payload. E-mails clients will display the resulting payload one part after another, with attachments displayed inline if their
inline
attribute is set.A
Mixed
instance can be created by combining two or moreMailItem
s with the&
operator:text = TextBody("Look at the pretty kitty!\n") image = BytesAttachment.from_file("snuffles.jpeg", inline=True) sig = TextBody("Sincerely, Me\n") mixed = text & image & sig
Likewise, additional
MailItem
s can be added to aMixed
instance with the&=
operator:# Same as above: mixed = Mixed() mixed &= TextBody("Look at the pretty kitty!\n") mixed &= BytesAttachment.from_file("snuffles.jpeg", inline=True) mixed &= TextBody("Sincerely, Me\n")
Using
&
to combine aMailItem
with astr
automatically converts thestr
to aTextBody
:# Same as above: image = BytesAttachment.from_file("snuffles.jpeg", inline=True) mixed = "Look at the pretty kitty!\n" & image & "Sincerely, Me\n" assert mixed.contents == [ TextBody("Look at the pretty kitty!\n"), BytesAttachment.from_file("snuffles.jpeg", inline=True), TextBody("Sincerely, Me\n"), ]
When combining two
Mixed
instances with&
or&=
, the contents are “flattened”:part1 = Mixed() part1 &= TextBody("Look at the pretty kitty!\n") part1 &= BytesAttachment.from_file("snuffles.jpeg", inline=True) part2 = Mixed() part2 &= TextBody("Now look at this dog.\n") part2 &= BytesAttachment.from_file("rags.jpeg", inline=True) part2 &= TextBody("Which one is cuter?\n") mixed = part1 & part2 assert mixed.contents == [ TextBody("Look at the pretty kitty!\n"), BytesAttachment.from_file("snuffles.jpeg", inline=True), TextBody("Now look at this dog.\n"), BytesAttachment.from_file("rags.jpeg", inline=True), TextBody("Which one is cuter?\n"), ]
-
class
eletter.
Related
(content=NOTHING, start: Optional[str] = None, *, content_id: Optional[str] = None)[source]¶ New in version 0.3.0.
A multipart/related e-mail payload. E-mail clients will display the part indicated by the
start
parameter, or the first part ifstart
is not set. This part may refer to other parts (e.g., images or CSS stylesheets) by their Content-ID headers, which can be generated usingemail.utils.make_msgid
.Note
Content-ID headers begin & end with angle brackets (
<...>
), which need to be stripped off before including the ID in the starting part.A
Related
instance can be created by combining two or moreMailItem
s with the^
operator:from email.utils import make_msgid img_cid = make_msgid() html = HTMLBody( "<p>Look at the pretty kitty!</p>" f'<img src="cid:{img_cid[1:-1]}"/>" "<p>Isn't he <em>darling</em>?</p>" ) image = BytesAttachment.from_file("snuffles.jpeg", content_id=img_cid) related = html ^ image
Likewise, additional
MailItem
s can be added to aRelated
instance with the^=
operator:# Same as above: img_cid = make_msgid() related = Related() related ^= HTMLBody( "<p>Look at the pretty kitty!</p>" f'<img src="cid:{img_cid[1:-1]}"/>" "<p>Isn't he <em>darling</em>?</p>" ) related ^= BytesAttachment.from_file("snuffles.jpeg", content_id=img_cid)
Using
^
to combine aMailItem
with astr
automatically converts thestr
to aTextBody
, though this is generally not all that useful, as you’ll usually want to createRelated
instances fromHTMLBody
s instead.When combining two
Related
instances with^
or^=
, the contents are “flattened”:# Same as above: img_cid = make_msgid() htmlrel = Related([ HTMLBody( "<p>Look at the pretty kitty!</p>" f'<img src="cid:{img_cid[1:-1]}"/>" "<p>Isn't he <em>darling</em>?</p>" ) ]) imgrel = Related([ BytesAttachment.from_file("snuffles.jpeg", content_id=img_cid) ]) related = htmlrel ^ imgrel assert related.contents == [ HTMLBody( "<p>Look at the pretty kitty!</p>" f'<img src="cid:{img_cid[1:-1]}"/>" "<p>Isn't he <em>darling</em>?</p>" ), BytesAttachment.from_file("snuffles.jpeg", content_id=img_cid), ]
Changed in version 0.4.0: Using
^
to combine aMailItem
with astr
now automatically converts thestr
to aTextBody
Decomposition¶
-
eletter.
decompose
(msg: email.message.EmailMessage) → eletter.decompose.Eletter[source]¶ New in version 0.5.0.
Decompose an
EmailMessage
into anEletter
instance containing aMailItem
and a collection of headers. Only structures that can be represented byeletter
classes are supported.All message parts that are not text/plain, text/html, multipart/*, or message/* are treated as attachments. Attachments without filenames or an explicit “attachment” Content-Disposition are treated as inline.
Any information specific to how the message is encoded is discarded (namely, “charset” parameters on text/* parts, Content-Transfer-Encoding headers, and MIME-Version headers).
Headers on message sub-parts that do not have representations on
MailItem
s are discarded (namely, everything other than Content-Type, Content-Disposition, and Content-ID).- Raises
TypeError – if any sub-part of
msg
is not anEmailMessage
instanceDecompositionError – if
msg
contains a part with an unrepresentable Content-Type
-
eletter.
decompose_simple
(msg: email.message.EmailMessage, unmix: bool = False) → eletter.decompose.SimpleEletter[source]¶ New in version 0.5.0.
Decompose an
EmailMessage
into aSimpleEletter
instance consisting of a text body and/or HTML body, some number of attachments, and a collection of headers. TheEmailMessage
is first decomposed withdecompose()
and then simplified by callingEletter.simplify()
.By default, a multipart/mixed message can only be simplified if all of the attachments come after all of the message bodies; set
unmix
toTrue
to separate the attachments from the bodies regardless of what order they come in.- Raises
TypeError – if any sub-part of
msg
is not anEmailMessage
instanceDecompositionError – if
msg
contains a part with an unrepresentable Content-TypeSimplificationError – if
msg
cannot be simplified
-
class
eletter.
Eletter
[source]¶ New in version 0.5.0.
A decomposed e-mail message
-
content
: eletter.classes.MailItem¶ The message’s body
-
from_
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s From addresses
-
to
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s To addresses
-
cc
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s CC addresses
-
bcc
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s BCC addresses
-
reply_to
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s Reply-To addresses
-
sender
: Optional[email.headerregistry.Address]¶ The message’s Sender address, if any
-
date
: Optional[datetime.datetime]¶ The message’s Date header, if set
-
headers
: Dict[str, List[str]]¶ Any additional headers on the message. The header names are lowercase.
-
compose
() → email.message.EmailMessage[source]¶ Convert the
Eletter
back into anEmailMessage
-
simplify
(unmix: bool = False) → eletter.decompose.SimpleEletter[source]¶ Simplify the
Eletter
into aSimpleEletter
, breaking downEletter.content
into a text body, HTML body, and a list of attachments.By default, a multipart/mixed message can only be simplified if all of the attachments come after all of the message bodies; set
unmix
toTrue
to separate the attachments from the bodies regardless of what order they come in.- Raises
SimplificationError – if
msg
cannot be simplified
-
-
class
eletter.
SimpleEletter
[source]¶ New in version 0.5.0.
A decomposed simple e-mail message, consisting of a text body and/or HTML body plus some number of attachments and headers
-
attachments
: List[eletter.classes.Attachment]¶ Attachments on the message
-
from_
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s From addresses
-
to
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s To addresses
-
cc
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s CC addresses
-
bcc
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s BCC addresses
-
reply_to
: List[Union[email.headerregistry.Address, email.headerregistry.Group]]¶ The message’s Reply-To addresses
-
sender
: Optional[email.headerregistry.Address]¶ The message’s Sender address, if any
-
date
: Optional[datetime.datetime]¶ The message’s Date header, if set
-
headers
: Dict[str, List[str]]¶ Any additional headers on the message. The header names are lowercase.
-
compose
() → email.message.EmailMessage[source]¶ Convert the
SimpleEletter
back into anEmailMessage
-
Exceptions¶
-
exception
eletter.errors.
Error
[source]¶ Bases:
Exception
New in version 0.5.0.
The superclass of all custom exceptions raised by
eletter
-
exception
eletter.errors.
DecompositionError
[source]¶ Bases:
eletter.errors.Error
,ValueError
New in version 0.5.0.
Raised when
eletter
is asked to decompose anEmailMessage
with an unrepresentable Content-Type
-
exception
eletter.errors.
SimplificationError
[source]¶ Bases:
eletter.errors.Error
,ValueError
New in version 0.5.0.
Raised when
eletter
is asked to simplify a message that cannot be simplified
-
exception
eletter.errors.
MixedContentError
[source]¶ Bases:
eletter.errors.SimplificationError
New in version 0.5.0.
Subclass of
SimplificationError
raised when a multipart/mixed is encountered in which one or more attachments precede a message body part; such messages can be forced to be simplified by setting theunmix
argument ofsimplify()
ordecompose_simple()
toTrue
.
Utility Functions¶
-
eletter.
assemble_content_type
(maintype: str, subtype: str, **params: str) → str[source]¶ New in version 0.2.0.
Construct a Content-Type string from a maintype, subtype, and some number of parameters
- Raises
ValueError – if
f"{maintype}/{subtype}"
is an invalid Content-Type
-
eletter.
format_addresses
(addresses: Iterable[Union[str, email.headerregistry.Address, email.headerregistry.Group]], encode: bool = False) → str[source]¶ Convert an iterable of e-mail address strings (of the form “
foo@example.com
”, without angle brackets or a display name),Address
objects, and/orGroup
objects into a formatted string. Ifencode
isFalse
(the default), non-ASCII characters are left as-is. If it isTrue
, non-ASCII display names are converted into RFC 2047 encoded words, and non-ASCII domain names are encoded using Punycode.
-
eletter.
reply_quote
(s: str, prefix: str = '> ') → str[source]¶ New in version 0.2.0.
Quote a text following the de facto standard for replying to an e-mail; that is, prefix each line of the text with
"> "
(or a custom prefix), and if a line already starts with the prefix, omit any trailing whitespace from the newly-added prefix (so"> already quoted"
becomes">> already quoted"
).If the resulting string does not end with a newline, one is added. The empty string is treated as a single line.
Changelog¶
v0.5.0 (2021-03-27)¶
Attachments’ filenames can now be
None
Added a
decompose()
function for decomposing anEmailMessage
into aMailItem
plus headersAdded a
decompose_simple()
function for decomposing anEmailMessage
into a text body, HTML body, attachments, and headersThe
subject
argument to thecompose()
function & method can now beNone
/omittedIf an address argument to
compose()
is set to an empty list, the corresponding header will no longer be present in the resulting e-mailGave
Related
aget_root()
method
v0.4.0 (2021-03-13)¶
Using
|
,&
, or^
on aMailItem
and astr
now automatically converts thestr
to aTextBody
The
from_
argument to thecompose()
function & method can now beNone
/omittedformat_addresses()
has been moved to mailbits but is still re-exported from this library for the time being.Breaking: All arguments to the
compose()
function & method are now keyword-only
v0.3.0 (2021-03-11)¶
Gave the
from_file()
classmethodsinline
andcontent_id
argumentsGave all classes optional
content_id
attributesAdded
TextBody
,HTMLBody
,Alternative
,Mixed
, andRelated
classes for constructing complex e-mails
v0.2.0 (2021-03-09)¶
Gave
BytesAttachment
andFileAttachment
each afrom_file()
classmethodThe
from_
andreply_to
arguments tocompose()
may now be passed lists of addressesSupport address groups
Added
assemble_content_type()
,format_addresses()
, andreply_quote()
utility functionsAdded an
EmailAttachment
class
v0.1.0 (2021-03-09)¶
Initial release
eletter
provides functionality for constructing & deconstructing
email.message.EmailMessage
instances without having to touch the needlessly
complicated EmailMessage
class itself. A simple function
enables composition of e-mails with text and/or HTML bodies plus attachments,
and classes are provided for composing more complex multipart e-mails.
Installation¶
eletter
requires Python 3.6 or higher. Just use pip for Python 3 (You have pip, right?) to install
eletter
and its dependencies:
python3 -m pip install eletter
Examples¶
Constructing an e-mail with the compose()
function:
import eletter
TEXT = (
"Oh my beloved!\n"
"\n"
"Wilt thou dine with me on the morrow?\n"
"\n"
"We're having hot pockets.\n"
"\n"
"Love, Me\n"
)
HTML = (
"<p>Oh my beloved!</p>\n"
"<p>Wilt thou dine with me on the morrow?</p>\n"
"<p>We're having <strong>hot pockets</strong>.<p>\n"
"<p><em>Love</em>, Me</p>\n"
)
with open("hot-pocket.png", "rb") as fp:
picture = eletter.BytesAttachment(
content=fp.read(),
filename="enticement.png",
content_type="image/png",
)
msg = eletter.compose(
subject="Meet Me",
from_="me@here.qq",
to=[eletter.Address("My Dear", "my.beloved@love.love")],
text=TEXT,
html=HTML,
attachments=[picture],
)
msg
can then be sent like any other EmailMessage
, say, by using
outgoing.
For more complex e-mails, a set of classes is provided. Here is the equivalent
of the HTML-with-image e-mail with alternative plain text version from the
email
examples page in the Python docs:
from email.utils import make_msgid
import eletter
text = eletter.TextBody(
"Salut!\n"
"\n"
"Cela ressemble à un excellent recipie[1] déjeuner.\n"
"\n"
"[1] http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718\n"
"\n"
"--Pepé\n"
)
asparagus_cid = make_msgid()
html = eletter.HTMLBody(
"<html>\n"
" <head></head>\n"
" <body>\n"
" <p>Salut!</p>\n"
" <p>Cela ressemble à un excellent\n"
' <a href="http://www.yummly.com/recipe/Roasted-Asparagus-'
'Epicurious-203718">\n'
" recipie\n"
" </a> déjeuner.\n"
" </p>\n"
f' <img src="cid:{asparagus_cid[1:-1]}" />\n'
" </body>\n"
"</html>\n"
)
image = eletter.BytesAttachment.from_file(
"roasted-asparagus.jpg",
inline=True,
content_id=asparagus_cid,
)
msg = (text | (html ^ image)).compose(
subject="Ayons asperges pour le déjeuner",
from_=eletter.Address("Pepé Le Pew", "pepe@example.com"),
to=[
eletter.Address("Penelope Pussycat", "penelope@example.com"),
eletter.Address("Fabrette Pussycat", "fabrette@example.com"),
],
)