Emailing Content to Django with Webfaction's mail2script

Update: in recent versions of Django, its much better to use custom management commands

Webfaction, my shared-hosting provider of choice, has a nice feature that allows you to pipe emails to a script. This is useful for all sorts of things, but I decided to try it out by building a way for a friend to email photos to the front page of his website, which is built with Django.

To start, create a python script (I called mine EmailPhotoUpload.py). You can place it anywhere really, but I placed mine right in the django app that had the models I would be working with. Make sure the script starts with

1
/usr/local/bin/python2.5

or whatever Python you're using with webfaction. Also, make sure it's executable:

1
chmod +x EmailPhotoUpload.py

When mail2script executes the script, it doesn't execute with knowledge about your Django project, so you'll need to set the DJANGO_SETTINGS_MODULE environment variable and place your project on your Python path if its not already. Something like this works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
PROJECT_ROOT = '/home/webfactionuser/webapps/django/yourproject/'
DJANGO_SETTINGS_MODULE = "settings"

def setup_django():
    sys.path.append(PROJECT_ROOT)
    keys = os.environ.keys()
    from re import search
    for key in keys:
        if not search("DJANGO_SETTINGS_MODULE", key):
        os.environ["DJANGO_SETTINGS_MODULE"] = DJANGO_SETTINGS_MODULE     

setup_django()

Fix the project root variable to point at your project.

Once you've done that, you can import the models you want to manipulate. I have a model called 'Photo' in the 'photo' app. So I import it like so:

1
from photo.models import Photo

The email is accessible at sys.stdin, which is a file-like object. Python's email module has a method that takes a file and makes a Message object, so this works out nicely:

1
msg = email.message_from_file(sys.stdin)

Now you can refer to the email module documentation to get your data. I started by writing a few helper functions to get the data I want out of email.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_text(msg):
    "Looks for a part of the message with a text/plain mime type and returns it"
    text = ""
    if msg.is_multipart():
        for part in msg.get_payload():
            if part.get_content_type() == 'text/plain':
                text = part.get_payload()
    else:
        text = msg.get_payload()
    return text

def get_jpegs(msg):
    "Looks for any part of the message with image/jpeg mime type and returns a list."
    jpegs = []
    if msg.is_multipart():
        for part in msg.get_payload():
            if part.get_content_type() == 'image/jpeg':
                data = part.get_payload()
                tempjpeg = tempfile.NamedTemporaryFile('w+b', -1)
                tempjpeg.write(base64.b64decode(data))
                jpegs.append(tempjpeg)
    return jpegs

A few things to note: I'm using Python's tempfile module to hold the jpegs until I can save them in my model. Django has its own NamedTemporaryFile class, but it's for Windows compatibility and since this is a linux environment, we can use the standard library's. All we need to do is take the payload from the message part, base64 decode it and write it into a file. If you expect large files, or many, you'll eat up memory here and you'll have to do something more sophisticated.

The Photo model I'm working with looks something like this:

1
2
3
4
5
6
class Photo(models.Model):
    title = models.CharField("Photo Title", max_length=255)
    original = models.ImageField("Image", upload_to="img/uploaded/original")
    resized = models.ImageField(upload_to="img/uploaded/resized")
    credit = models.CharField("Credit", max_length=255, blank=True)
    caption = models.TextField("Description", blank=True)

I'm going to use the From field to populate the photo credit, the text portion of the email to populate the caption, the subject line of the email to populate the photo's title.

1
2
3
4
5
sender = re.sub("\s<(.+)>(\s*)$", "", get_header(msg, 'From'))
subject = get_header(msg, 'Subject')
text = get_text(msg) 
jpegs = get_jpegs(msg)
basename = re.sub(r"[\\/ \(\):?;,]", "_", subject)

For any of the header information, just call get_header, pass in the name of the email header and then perform any clean up on the result. For instance, I'm stripping the email address portion from the "From" header. Now just iterate through your list of jpegs and make a Photo object for each:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for index, jpeg in enumerate(jpegs):
    photo = Photo()
    photo.title = subject
    photo.caption = text
    photo.credit = sender
    filename = "".join([basename, str(index), '.jpg'])
    photo.original.save(filename, File(jpeg))
    photo.promote = True
    photo.save()
    jpeg.close()

Remember, jpegs is a list of temporary files; be sure to close them so the os cleans them up.

Now log in to webfaction, create an email address and put the absolute path to this script in the target area.

Please note, this isn't very secure. Anyone with the address can post content, and if you're not cleaning it, they may be able to do worse--never trust user input.

I plan on releasing a cleaner, more reusale classed-based version with some added features and exception handling, but for now I'll post the full text of what I describe above so you can see everything in context. Remember, you can test your script by downloading the raw email text and redirecting it to your script at the command line like this:

1
./EmailPhotoUpload.py < test-msg.txt

Good luck!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
:::/usr/bin/env python

import os
import re
import sys
import email
import base64
import tempfile
from django.core.files import File

PROJECT_ROOT = '/home/justin/Documents/sites/workshop/workshop/'
DJANGO_SETTINGS_MODULE = "settings" 

def setup_django():
    sys.path.append(PROJECT_ROOT)
    keys = os.environ.keys()
    from re import search
    for key in keys:
        if not search("DJANGO_SETTINGS_MODULE", key):
            os.environ["DJANGO_SETTINGS_MODULE"] = DJANGO_SETTINGS_MODULE     

def get_text(msg):
    text = ""
    if msg.is_multipart():
        for part in msg.get_payload():
            if part.get_content_type() == 'text/plain':
                text = part.get_payload()
    return text

def get_jpegs(msg):
    jpegs = []
    if msg.is_multipart():
        for part in msg.get_payload():
            if part.get_content_type() == 'image/jpeg':
                data = part.get_payload()
                tempjpeg = tempfile.NamedTemporaryFile('w+b', -1)
                tempjpeg.write(base64.b64decode(data))
                jpegs.append(tempjpeg)
    return jpegs

# Start
setup_django()
from photo.models import Photo
msg = email.message_from_file(sys.stdin)

address = msg.get('Return-Path', '').strip("<>")
sender = re.sub("\s<(.+)>(\s*)$", "", msg.get('From', 'Unknown'))
subject = msg.get('Subject', 'Untitled')
text = get_text(msg) 
jpegs = get_jpegs(msg)
basename = re.sub(r"[\\/ \(\):?;,]", "_", subject)

for index, jpeg in enumerate(jpegs):
    photo = Photo()
    photo.title = subject
    photo.caption = text
    photo.credit = sender
    filename = "".join([basename, str(index), '.jpg'])
    photo.original.save(filename, File(jpeg))
    photo.promote = True
    photo.save()

for jpeg in jpegs:
    jpeg.close()

2009-12-21