Docsity
Docsity

Prepare for your exams
Prepare for your exams

Study with the several resources on Docsity


Earn points to download
Earn points to download

Earn points by helping other students or get them with a premium plan


Guidelines and tips
Guidelines and tips

Mailthon Documentation: User's Guide and Developer Reference, Study notes of Philosophy

Computer ScienceEmail communicationPython Programming

An introduction to Mailthon, a Python library for sending emails. It covers getting started with Mailthon, creating emails, and the Mailthon API. The document also includes a comparison between using Mailthon and the standard library smtplib module, as well as information on IDNA and Friends. For developers, there is a detailed reference to classes, methods, and functions in Mailthon.

What you will learn

  • How does Mailthon compare to the standard library smtplib module?
  • How do you create an email using Mailthon?
  • What is IDNA and how does Mailthon handle it?
  • What is Mailthon and what is it used for?
  • What is the role of the Headers class in Mailthon?

Typology: Study notes

2021/2022

Uploaded on 09/12/2022

aeinstein
aeinstein 🇺🇸

4.6

(20)

19 documents

1 / 28

Toggle sidebar

Related documents


Partial preview of the text

Download Mailthon Documentation: User's Guide and Developer Reference and more Study notes Philosophy in PDF only on Docsity! Mailthon Documentation Release 0.1.0 Eeo Jun August 22, 2016 Mailthon Documentation, Release 0.1.0 Welcome to Mailthon’s documentation. Mailthon is an email library for Python that aims to be highly extensible and composable. The docs are divided into different parts. I suggest that you start with Installation and then head to the Quickstart. If you want to dive into the internals, go for the API section. Contents 1 Mailthon Documentation, Release 0.1.0 2 Contents CHAPTER 1 User’s Guide This part of the documentation, mostly consisting of prose, begins with background information of Mailthon, and focuses on getting started with the library. 1.1 Foreword Read this before getting started with Mailthon. It answers (hopefully) some questions about the goals of the project and whether you should consider using it or not. 1.1.1 Philosophy Mailthon’s philosophy is that composability and extensibility stem from an elegant and simple codebase. Also, one of Mailthon’s aims is to not include as much magic as practically possible, but remain pragmatic at the same time. For example Mailthon does not have magical HTML/plaintext inference of the content- that is left to the developer to decide. Another value is the sanity of the API- whether something is named intuitively, or whether something should work like it does. The focus is on having as little surprises as possible. 1.1.2 Code over Configuration While some minimum configuration can be done via “configuration”, most of the time Mailthon strives to encourage code-over-configuration. This is because code is always more powerful than configuration, which is one of the driving ideals behind middleware classes. Also, Mailthon does not rely on any configuration files, nor does it try to parse any files. Any configuration should be made explicit in the code. Continue to the Installation or the Quickstart. 1.2 Installation Mailthon does not have any external dependencies apart from the Python standard library. Mailthon is however, only tested against Python 2.6 or newer, so make sure you have an up-to-date Python installation. Mailthon also supports Python 3. 3 Mailthon Documentation, Release 0.1.0 receivers=['receiver@mail.com'], subject='Hello World!', content='Hi!', ) However take note that the email() function makes an email with a HTML enclosure, not the plaintext one like what we’ve just created. An alternative to building the headers by hand is to use the mailthon.headers module: import mailthon.headers as headers Envelope( headers=[ headers.sender('sender <username@mail.com>'), headers.to('receiver@mail.com'), headers.subject('Hello World!'), ], enclosure=[], ) 1.3.3 Creating a Postman Mailthon uses rather quirky real-life names to represent abstract email concepts. What better name for something that delivers an envelope than a postman? To create a postman that is configured for a GMail server: from mailthon.postman import Postman from mailthon.middleware import TLS, Auth postman = Postman( host='mail.google.com', port=587, middlewares=[ TLS(force=True), Auth(username='USERNAME', password='PASSWORD') ], ) (Substitute USERNAME and PASSWORD with your credentials, obtained from the Authorizing applications & sites page.) So what did we just do? 1. We created a Postman instance. A Postman handles the sending of emails via some transport, usually that defaults to SMTP. The Postman is created with the correct host and port arguments. 2. We configured the Auth and TLS middleware. They provide authentication and TLS support, respectively. We also forced TLS because we know that the GMail SMTP server only allows us to login if we have TLS enabled (which is also the reason why it is placed before the authentication middleware). There is again, a simpler function for handling that in the form of the postman() function: from mailthon import postman as postman_ postman = postman_( host='mail.google.com', port=587, force_tls=True, auth=('USERNAME', 'PASSWORD'), ) 6 Chapter 1. User’s Guide Mailthon Documentation, Release 0.1.0 1.3.4 Sending an Envelope After creating an envelope and a postman, we can then send the envelope to the receivers using the send() method: response = postman.send(envelope) Which returns the result of the sending the envelope- whether the server accepted it, whether everything went OK, etc. You can access the response values: print(response.message) print(response.status_code) if response.ok: print("OK! :)") You might want to continue reading about Mailthon’s architecture in the In Depth Guide, or dive into the internals in the API section. 1.4 In Depth Guide This document describes the API in a stage-by-stage basis. It is useful as a book-like, gentle technical introduction to the higher and lower level APIs. Before reading this guide it is recommended that you browse through the Quickstart as it will give a very high level introduction to Maithon. If you want to look for some method or class, go to the API section. 1.4.1 Envelopes and MIMEs The Envelope class actually wraps around multipart MIME objects. This means they can be composed of multiple other MIME objects, e.g. plaintext MIMEs, MIMEs containing HTML data, images, etc, which provides the perfect building block for a composable class. Envelopes are made of two parts- like a real life envelope, a “stamp”, or headers, and an enclosure, made of other mime objects: from mailthon.envelope import Envelope from mailthon.enclosure import PlainText e = Envelope( headers={'X-Key': 'Value'}, enclosure=[PlainText('something')], ) An interesting thing to take note of is that envelopes can be enclosed within envelopes. Concretely speaking, Envelopes consist of the Headers class and a list of Enclosure objects: >>> e.headers {'X-Key': u'Value'} >>> e.enclosure [<mailthon.enclosure.PlainText object at 0x...>] You might have noticed that the Value string that was set was changed to a Unicode value. Why is that so? This is because internally the Headers class decodes bytes values that we throw at it into Unicode objects, freeing the developer from any headaches about encoding. You can read more about these design decisions at the Design Decisions section. Now that we’ve looked at the higher level API of the Envelope class, let’s plunge deeper into madness and look into how it generates MIME objects with the mime() method: 1.4. In Depth Guide 7 Mailthon Documentation, Release 0.1.0 >>> e.mime() <email.mime.multipart.MIMEMultipart object at 0x...> 1. Generates a MIMEMultipart instance and attaches each of the enclosures with the attach() method. Conceptually this is what you’d do with a real envelope- put each of the content into the enclosure of the envelope. 2. Puts a stamp on the envelope- sets the headers onto the envelope object. This is done via the prepare() method of the headers object, which handles setting the appropriate headers- e.g. it ignores the Bcc headers to save you from embarassment and also to make Mailthon compliant with RFC 2822. 1.4.2 Disecting Enclosures Conceptually the Envelope and Enclosure classes are the same- they are both made out of headers and some content. API-wise, they are also nearly identical- they both provide the same mime() method. And you are right! Here we see that the enclosure objects do in fact have almost the same attributes: >>> plaintext = PlainText('content') >>> plaintext.headers {} >>> plaintext.mime() <email.mime.text.MIMEText object at 0x...> However, speaking from a responsibility perspecitive, here is where they differ. Envelopes have the concept of senders and receivers- and must keep track of them. Enclosures however, are like a superset of envelopes- an envelope can be an enclosure, but not the other way round, (at least, without some tricks). All Enclosures have a content attribute that represents the content of the enclosure. This is once again something that the envelope object doesn’t have: >>> plaintext.content 'content' The role as a MIME-preparing class is the same. As mentioned earlier, both classes have the mime() method which prepares a MIME object- needless to say different subclasses of the Enclosure class handle different mimetypes, e.g. PlainText handles text/plain content. Similarly this is what an enclosure class does when it’s mime() method is called: 1. Prepare the MIME object. For PlainText enclosures this returns a MIMEText object. For Binary enclo- sures the method returns a MIMEBase object which is a lower level but more configurable and flexible version of the MIMEText class. 2. Apply the headers. Conceptually this is where the envelope analogy breaks down- you don’t usually have stamps inside enclosures, but let’s pretend that didn’t happen. The Enclosure object is designed in such a way such that the subclasses will not need to worry about applying the user’s headers. Essentially what the mime() method looks like is: def mime(self): mime = self.mime_object() self.headers.prepare(mime) return mime Which means that you usually do not have to worry about any headers that you’ve set not being applied to the generated MIME objects. So if you were to subclass the enclosure class: class Cat(Enclosure): def mime_object(self): return make_mime(self.cat_name) 8 Chapter 1. User’s Guide Mailthon Documentation, Release 0.1.0 1. Local-Part which can be UTF-8 encoded as per RFC 6531. The local part is not really important to the sending server who you are sending it to, rather it is more concerned with which server you are sending it to. 2. Domain-Part which should be IDNA-encoded. Although servers which are compliant with both RFC 6531 and RFC 6532 can accept Unicode-encoded domain names, the pessimistic guess would be that most aren’t, so for the time being we are encoding in IDNA. Putting it all together we have something like the following function: def stringify_address(addr): localpart, domain = addr.split('@', 1) return b'@'.join([ localpart.encode('utf8'), domain.encode('idna'), ]) But Mailthon already has a more robust implementation available in the form of the stringify_address() function, and is automatically used by the Postman class when sending envelopes. Via the sendmail() method. Essentially, the following: def send(smtp, envelope): smtp.sendmail( stringify_address(envelope.sender), [stringify_address(k) for k in envelope.receivers], envelope.string(), ) Which explains why the addresses specified in the mail_from and receivers attributes must be Unicode values instead of byte strings since mixing them up will cause issues in Python 3. 1.4.4 The Postman Object The Postman class is responsible for delivering the email via some transport, and is meant as a transport-agnostic layer to make sending emails via different protocols as painless as possible. Let’s start by creating a postman instance: >>> from mailthon.postman import Postman >>> postman = Postman(host='smtp.server.com', port=587) The Mutation Phase The transport attribute. This is the actual “transport” used to send our emails over to a real server. To implement a transport it turns out that we need, at the very least, to have the ehlo, noop, quit, and sendmail methods: class MyTransport(object): def __init__(self, host, port): self.host = host self.port = port self.connection_started = False def check_conn(self): if not self.connection_started: raise IOError def noop(self): self.check_conn() return 200 1.4. In Depth Guide 11 Mailthon Documentation, Release 0.1.0 def ehlo(self): self.connection_started = True def sendmail(self, sender, receipients, string): self.check_conn() return {} def quit(self): self.connection_started = False Next all we need to do is replace the tranport attribute with the class object that we’ve just created. Although this is not recommended as I recommend subclassing to change the transport being used we will do it anyways: postman.transport = MyTransport The response_cls attribute will contain a custom response class. We will create our own response class as well: class Response(object): def __init__(self, rejected, status): self.rejected = rejected self.status_code = status @property def ok(self): return self.status_code == 200 and \ not self.rejected If you haven’t noticed, the __init__ method of our custom response class matches perfectly with the return values of the sendmail and noop methods from the MyTransport class, respectively. They are called by the Postman class like so: def deliver(self, conn, envelope): rejected = conn.sendmail(...) return self.response_cls(rejected, conn.noop()) Now we just have to change the response class on the postman object we’ve created. Once again I recommend subclassing to change these attributes but for this experiment we’ll change them in runtime: >>> postman.response_cls = Response Putting it all together Next we’ll send an envelope “across the wire” using our mutated postman object with our custom transport and response classes: >>> r = postman.send(envelope) >>> assert r.ok But that doesn’t give us very much knowlegde of what happens underneath the hood. The send() method is simply a veneer over the lower level connection() and deliver() methods. Let’s recreate the send method: >>> with postman.connection() as conn: ... print(conn.connection_started) ... r = postman.deliver(conn, envelope) ... print(r) ... True <__main__.Response object at 0x...> 12 Chapter 1. User’s Guide Mailthon Documentation, Release 0.1.0 Basically what the connection() context manager does is that it manages the (usually SMTP) session for you. It is roughly implemented as: @contextmanager def connection(self): conn = self.transport(self.host, self.port) try: conn.ehlo() yield conn finally: conn.quit() Which closes the connection regardless of whether the sending operation is a success. This is important to prevent excessive memory and file-descriptor usage from the open sockets. You can verify that the connection as closed: >>> conn.connection_started False Which is changed to False due the the context manager calling the quit method once the block of code within the with statement has finished executing. If you would like to find out how all of this is implemented you can take a look at the source code. 1.4.5 Middlewares and Middlemen One of the more powerful features of Mailthon is the ability to add middleware- which are basically functions that allow for certain features, e.g. TLS, Auth which provide for TLS and authentication, respectively. Let’s make our own middleware to see how all of this is done: def my_middleware(must_have=()): def func(conn): for item in must_have: assert hasattr(conn, item) return func Then we need to put our middleware in what’s known as a middleware stack. It is basically a list of callables which will be invoked with the transport object. Using our Postman class: postman.use(my_middleware(['quit'])) Which will add the closure into the middleware stack and assert that the transport object has the quit attribute/method. More powerful middleware can certainly be programmed via classes, the recommended way if you want to make extensible middlewares is to subclass from the Middleware class: from mailthon.middleware import Middleware class MyMiddleware(Middle): def __call__(self, conn): pass The registered middlewares will be called by the connection() method to set up the connection. If any exception is raised, the connection is automatically closed. 1.4. In Depth Guide 13 Mailthon Documentation, Release 0.1.0 class HTTPPostman(Postman): response_cls = MyResponse It will be called with the response returned by the noop() method, or the noop method of your own transport class. connection(*args, **kwds) A context manager that returns a connection to the server using some transport, defaulting to SMTP. The transport will be called with the server address, port, and options that have been passed to the constructor, in that order. deliver(conn, envelope) Deliver an envelope using a given connection conn, and return the response object. Does not close the connection. send(envelope) Sends an envelope and return a response object. use(middleware) Use a certain callable middleware, i.e. append it to the list of middlewares, and return it so it can be used as a decorator. Note that the middleware is added to the end of the middlewares list, so it will be called last. 2.1.2 Envelope Object class mailthon.envelope.Envelope(headers, enclosure, mail_from=None) Encapsulates the concept of an Envelope- there can be multiple stamps (headers) and multiple “things” inside the enclosure. Parameters • headers – An iterable/mapping of headers. • enclosure – A list of enclosure objects. mail_from Dictates the sender argument being passed to the SMTP.sendmail method. This is different from the sender property as it is the real sender, but the sender property is merely what appears on the email. If it is not set, the real sender is then inferred from the headers. mime() Returns a mime object. Internally this generates a MIMEMultipart object, attaches the enclosures, then prepares it using the internal headers object. receivers Returns a list of receiver addresses. sender Returns the sender of the envelope, obtained from the headers. string() Returns the MIME object as a string- i.e., calls the as_string method of the generated MIME object. 2.1.3 Enclosure Objects class mailthon.enclosure.Enclosure(headers=()) Base class for Enclosure objects to inherit from. An enclosure is a part of the enclosure in a real envelope- it contains part of the content to be sent. 16 Chapter 2. Developer Reference Mailthon Documentation, Release 0.1.0 Parameters headers – Iterable of headers to include, stored in an RFC-compliant Headers map- ping internally under the headers attribute. headers A Header instance containing all of the headers. Note that when the enclosure objects are prepared (i.e. the mime() method is called), this headers attribute has precendence over other headers already set in the mime_object() method. For example: e = EnclosureSubclass( mimetype='text/plain', headers={ 'Content-Type': 'text/html', }) mime = e.mime() assert mime['Content-Type'] == 'text/html' This is to ensure that the user ultimately has control over which headers are set in the final, prepared MIME object. content Anything the the Enclosure object contains. For example for PlainText enclosures this is usually a Unicode or bytes string. There is no restriction on what kind of object the content attribute may contain, but most of the time assume it is a string unless absolutely necessary, e.g. for some performance/memory- usage tweaks. On the Enclosure base class this defaults to None. mime() Returns the finalised mime object, after applying the internal headers. Usually this is not to be overriden. mime_object() To be overriden. Returns the generated MIME object, without applying the internal headers. class mailthon.enclosure.PlainText(content, encoding=’utf-8’, **kwargs) Enclosure that has a text/plain mimetype. Parameters • content – Unicode or bytes string. • encoding – Encoding used to serialize the content or the encoding of the content. • headers – Optional headers. class mailthon.enclosure.HTML(content, encoding=’utf-8’, **kwargs) Subclass of PlainText with a text/html mimetype. class mailthon.enclosure.Binary(content, mimetype, encoding=None, encoder=<function en- code_base64>, **kwargs) An Enclosure subclass for binary content. If the content is HTML or any kind of plain-text then the HTML or PlainText Enclosures are receommended since they have a simpler interface. Parameters • content – A bytes string. • mimetype – Mimetype of the content. • encoding – Optional encoding of the content. • encoder – An optional encoder function. • headers – Optional headers. 2.1. API 17 Mailthon Documentation, Release 0.1.0 class mailthon.enclosure.Attachment(path, headers=()) Binary subclass for easier file attachments. The advantage over directly using the Binary class is that the Content-Disposition header is automatically set. Parameters • path – Absolute/Relative path to the file. • headers – Optional headers. content Lazily returns the bytes contents of the file. 2.1.4 Response Objects class mailthon.response.Response(pair) Encapsulates a (status_code, message) tuple returned by a server when the NOOP command is called. Parameters pair – A (status_code, message) pair. status_code The integer status code returned by the server. You can look up what they mean in the SMTP reply codes page. message The accompanying message returned by the server, as an undecoded bytes object. The message usually differs on a per-server basis. ok Tells whether the Response object is ok- that everything went well. Returns true if the status code is 250, false otherwise. class mailthon.response.SendmailResponse(pair, rejected) Encapsulates a (status_code, message) tuple as well as a mapping of email-address to (status_code, message) tuples that can be attained by the NOOP and the SENDMAIL command. Parameters • pair – The response pair. • rejected – Dictionary of rejected addresses to status-code message pairs. rejected A dictionary of rejected addresses to Response objects. ok Returns True only if no addresses were rejected and if the status code is 250. 2.1.5 Middlewares class mailthon.middleware.Middleware Base class for middlewares. Middlewares are encouraged to subclass from this class for extensibility reasons. __call__(connection) To be overriden by subclasses. This method is called by the Postman class when a connection to the server is established with the transport instance, e.g. an smtplib.SMTP object. class mailthon.middleware.Auth(username, password) Middleware implementing authentication via LOGIN. Most of the time this middleware needs to be placed after TLS. 18 Chapter 2. Developer Reference Mailthon Documentation, Release 0.1.0 env = Envelope(...) for server in servers: preparer[server](env) env.deliver(**options[server]) But then again you run into issues like “did I change this attribute before I delivered it?” Which ultimately leads to hard to find, hard to debug errors. 2. You want to send multiple envelopes but using the same server configuration and middleware. Once again this can be solved via some preparation functions: for env in envelopes: prepare(env) env.deliver(**options) Once again this leads to conceptual issues and practical issues. For example it is cumbersome to keep track of some configuration just to pass it to a method. Also it is extremely unweildly to have a giant Envelope class that does everything. 2.2.2 Unicode Headers This design decision is in fact inspired by Werkzeug. Basically, SMTP allows you to specify headers of different encodings, e.g. UTF-8 headers for chinese characters in the subject field. While the Message objects allow you to pass in unicode or byte strings, or even Header objects as the header values- Mailthon chooses to pass in Unicode values. The background is simple- so that users writing special headers do not need to worry about weird encoding problems that do not usually show up until the time where you least expect them to. Thus the UnicodeDict class was born: >>> from mailthon.helpers import UnicodeDict >>> d = UnicodeDict() >>> d['uni'] = u'unicode' >>> d['uni'] u'unicode' >>> d['key'] = b'value' >>> d['key'] u'value' Which automatically tries to decode bytes values into Unicode strings. This makes development of Headers very painless; you can pass in whatever value in Unicode- they will all look the same and thus can be very easily pro- grammed against. Also it makes passing in a special mail_from parameter to the Envelope‘ class simpler; you do not need to worry about encoding since the Postman object encodes it for you behind the scenes, using the stringify_address() function. 2.2. Design Decisions 21 Mailthon Documentation, Release 0.1.0 22 Chapter 2. Developer Reference Index Symbols __call__() (Middleware method), 18 A Attachment (class in mailthon.enclosure), 17 Auth (class in mailthon.middleware), 18 B bcc() (in module mailthon.headers), 20 Binary (class in mailthon.enclosure), 17 C cc() (in module mailthon.headers), 20 connection() (mailthon.postman.Postman method), 16 content (Enclosure attribute), 17 content (mailthon.enclosure.Attachment attribute), 18 D date() (in module mailthon.headers), 20 deliver() (mailthon.postman.Postman method), 16 E Enclosure (class in mailthon.enclosure), 16 Envelope (class in mailthon.envelope), 16 F format_addresses() (in module mailthon.helpers), 19 G guess() (in module mailthon.helpers), 19 H Headers (class in mailthon.headers), 19 headers (Enclosure attribute), 17 HTML (class in mailthon.enclosure), 17 M mail_from (mailthon.envelope.Envelope attribute), 16 message (Response attribute), 18 message_id() (in module mailthon.headers), 20 Middleware (class in mailthon.middleware), 18 mime() (mailthon.enclosure.Enclosure method), 17 mime() (mailthon.envelope.Envelope method), 16 mime_object() (mailthon.enclosure.Enclosure method), 17 O ok (mailthon.response.Response attribute), 18 ok (mailthon.response.SendmailResponse attribute), 18 P PlainText (class in mailthon.enclosure), 17 Postman (class in mailthon.postman), 15 prepare() (mailthon.headers.Headers method), 19 R receivers (mailthon.envelope.Envelope attribute), 16 receivers (mailthon.headers.Headers attribute), 19 rejected (SendmailResponse attribute), 18 resent (mailthon.headers.Headers attribute), 19 Response (class in mailthon.response), 18 response_cls (Postman attribute), 15 RFC RFC 2821, 9 RFC 2822, 8, 19 RFC 3490, 10 RFC 6531, 11 RFC 6532, 11 S send() (mailthon.postman.Postman method), 16 sender (mailthon.envelope.Envelope attribute), 16 sender (mailthon.headers.Headers attribute), 19 sender() (in module mailthon.headers), 20 SendmailResponse (class in mailthon.response), 18 status_code (Response attribute), 18 string() (mailthon.envelope.Envelope method), 16 stringify_address() (in module mailthon.helpers), 19 23
Docsity logo



Copyright © 2024 Ladybird Srl - Via Leonardo da Vinci 16, 10126, Torino, Italy - VAT 10816460017 - All rights reserved