Python ♥ Email

So my friend called me about a week ago. He asked me to set up a mailing list service for his domain I manage.

"One thing though: I only want specific people to be able to send e-mail to the list."

Could I have found a system that has a nice GUI and does this and more? Sure. The real question is: was I going to? Absolutely not.


In this article I will tell you how to forward email with python.
On how to forward email to a script in postfix, read my previous post.

You can check my final code on github.

The project

A python script that processes incoming email and forwards it based on the sender and the incoming email's recipient.

Receiving email

After postfix forwards email, we can get it in python through stdin like so:

import sys
email_in = sys.stdin.read()

This will give us a string of the whole email, headers and all.
The next step is to parse it into a Message object. This will enable us to perform all kinds of email tricks on it (like extracting From and To addresses)

From the python documentation:

The central class in the email package is the Message class, imported from the email.message module. (...) Message provides the core functionality for setting and querying header fields, and for accessing message bodies.

Let's do the actual parsing:

from email.parser import Parser
incoming = Parser().parsestr(email_in)

incoming is now a Message object that we can work with.

Getting the headers

Now that we have a Message object called incoming, it's pretty simple to get stuff from it, like the sender or recipient of the email:

sender = incoming['from']
this_address = incoming['to']

We will set up our script to look at the original address the email was sent to and decide which mailing list to work with based on that.
We will use the parseddr function to do that:

from email.utils import parseaddr
to_list = parseaddr(incoming['to'])[1]

parseaddr() returns a list of display name and address, for example:

toaddress = ("John Doe", "johndoe@example.com")

That's why we needed the [1] at the end, to get the email address as a string.

Now we need to get the part before the @ to see which list the mail was sent to:

list_user = to_list.split('@')[0]

We now have the list name and the sender, so we can go on and perform our checks to see if the list exists and the sender is authorized to use it.

Checking list and sender authorization

Our next step is to check if the list actually exists.

As we will need to parse files twice, let's write a function for that:

def readlist(file_path):
    with open(file_path, mode="r") as f:
        lines = f.readlines()
        result = [i.strip() for i in lines]
    return result

This simple function reads the contents of the given file and returns a list of text on each line.

Note the list comprehension, this is one of the things why I love python.

First we will try to read the mailing list file:

try:
    list_members = readlist(list_user + ".list")
except FileNotFoundError:
    bounce(nolist, incoming)
    #If the file is not found we will send a "bounce" email
    #stating the list does not exist.
    #We can write this function later.

Next we will load the authorized senders file:

senders = readlist("senders.list")

Now let's check if the sender is authorized to use this mailing list:

 #We get the sender email from the headers the same way
 #we did with the mailing list address:

 sender_addr = parseaddr(sender)[1]

 #Then we perform the test:

 if sender_str not in senders:
     bounce(nonauth, incoming)
 else:
     #The sender is authorized to send
     #Here we will send the actual email

Sending email

Now that we checked if the list exists and that the sender is on our "authorized senders" list it's time to forward the email to all recipients.

First some imports:

 import smtplib
 from email.mime.multipart import MIMEMultipart

We will use smtplib to send email through SMTP. MIMEMultipart is for managing Message objects.

The following goes into the else: part of our last if statements:

for member in list_members:
    #Iterate through the list of recipients

    #Create a Message object
    msg = MIMEMultipart()
    #Set the payload to what we received
    msg.set_payload(incoming)

    #Change headers so From is the list address
    #and Reply-to is original sender
    msg['From'] = this_address
    msg['reply-to'] = sender
    msg['To'] = member
    msg['Subject'] = incoming['subject']

    #Create an SMTP object on localhost
    s = smtplib.SMTP('localhost')

    #Send message, and close the SMTP connection
    s.send_message(msg)
    s.quit()

Final touches

The only thing left is to write the bounce function we used previously.
The steps will be very similar to how we sent email to the list:

from email.mime.text import MIMEText

def bounce(bouncetext, incoming):
    msg = MIMEMultipart()
    msg['Subject'] = "Re: " + incoming['subject']
    msg['From'] = incoming['to']
    msg['To'] = incoming['from']
    msg.attach(MIMEText(bouncetext, _charset='UTF-8'))
    s = smtplib.SMTP('localhost')
    s.send_message(msg)
    s.quit()
    exit(0)

Finally let's see all the imports we used:

import sys
from email.parser import Parser
from email.utils import parseaddr
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

Now we add a "shebang" to the first line of our script to make it executable outside the python interpreter:

#!/usr/bin/env python3

And we are done. Upload the script to your server and off the emails go.

Good job!


If you get stuck, it's always wise to read the docs:


I have a tiny newsletter you could subscribe to.

I am also on twitter, see you there.
@fonorobert