Package: django-inbound-email
Django Inbound EmailThis package now supports Python3 and Django1.11 or above. For previous version support please refer to the Python2 branch.An inbound email handler for Django.Current StatusWe have a working implementation, with SendGrid, Mailgun and Mandrill backends.
(Both SendGrid and Mandrill have been used in production environments.)The test_app is deployed to Heroku and any emails sent toanything@django-inbound-email.yunojuno.comwill be parsed by a live version of
the test_app.This test app just does a bounceback - any incoming email to that address
is sent back to the sender’s address. This makes it easy to test ‘real-world’
email examples. Please feel free to send email to that address - and if you’re
using Outlook 97 on Windows XP, in China, then we’d really like to hear from
you. You don’t hear that much on Github.NB The app deployed on Heroku currently sends the bouncebacks using the
Mailtrap app, so you won’t actually receive the email, but I can give limited
access to contributors so that they can see them all. I’ll truncate the Mailtrap
inbox at irregular intervals.What?A Django app to make it easy to receive inbound emails from users via a
hosted transactional email service (e.g. SendGrid, Postmark, Mandrill,
etc.)Contained within this project is the django app itself, together with
a working Django project that uses the app, and is separately deployable
to Heroku for testing purposes. The app has good test coverage, but it’s
really very hard to test inbound emails without having real data, and
that requires a public endpoint that you can use to hook up your
preferred email provider’s webhooks.Why?If your project accepts inbound emails, you are probably using one of
the big transactional email providers.These services all provide a mechanism for receiving inbound emails
which involves them (the service) parsing the inbound email and then
posting the contents to an HTTP endpoint in your project. This is a
great service, but it can often be fiddly to integrate into your app and
it reinforces service lock-in, as each service’s callback is slightly
different.There is also a significant SMTP-HTTP ‘impedance mismatch’. You send
emails through Django’s (SMTP) mail library, which provides the
EmailMessage and EmailMultiAlternative objects, but you receive emails
as an HTTP POST. Wouldn’t it be nice if you could both send and receive
Django objects?This app converts the incoming HttpRequest back into an
EmailMultiAlternatives object, and fires a signal that sends both the
new object, and the original request object. You simply have to listen
for this signal, and process the email as your application requires.The mail parser handles non-UTF-8 character sets (so those pesky PC
Outlook emails don’t come through all garbled), and file attachments.How?Although this is Django app, it contains (for now) no models. Its
principle component is a single view function that does the parsing.
There is a single configuration setting -INBOUND_EMAIL_PARSER,
which must be one of the supported backends.This setting is expected to be available to the app fromdjango.conf.settings,
and the app will raise an error if it does not exist.The default URL for inbound emails is simply ‘/inbound/’.The flow through the app is very simple:The app view functionreceive_inbound_emailis called when a new email
POST is received from your service provider.This function looks up theINBOUND_EMAIL_PARSER, loads the
appropriate backend, and parses therequest.POSTcontents out
into a newdjango.core.mail.EmailMultiAlternativesobject.Theemail_receivedsignal is fired, and the newEmailMultiAlternativesinstance is passed, along with the
originalHttpRequest(in case there’s any special handling that
you require - e.g. DKIM / SPF info, if your provider passes that
along).If an email is unacceptable in some way (e.g. an attachment is too large),
then theemail_received_unacceptablesignal is fired instead. This signal
has an argumentexceptiondescribing the problem.InstallationFor use as the app in Django project, usepip:$pipinstalldjango-inbound-emailFor hacking on the project, pull from Git:$gitpullgit@github.com:yunojuno/django-inbound-email.git$cddjango-inbound-emaildjango-inbound-email$# use virtualenvwrapper, and install Django to allow tests to rundjango-inbound-email$mkvirtualenvdjango-inbound-email(django-inbound-email)django-inbound-email$pipinstalldjangoUsageYour main concern, after installing and configuring the app, is handling
theemail_receivedsignal:# This snippet goes somewhere inside your project,# wherever you need to react to incoming emails.importloggingfrominbound_email.signalsimportemail_receiveddefon_email_received(sender,**kwargs):"""Handle inbound emails."""email=kwargs.pop('email')request=kwargs.pop('request')# your code goes here - save the email, respond to it, etc.logging.debug("New email received from%s:%s",email.from_email,email.subject)# pass dispatch_uid to prevent duplicates:# https://docs.djangoproject.com/en/dev/topics/signals/email_received.connect(on_email_received,dispatch_uid="something_unique")Handling file attachments as FileField propertiesThere is one gotcha in the handling of file attachments. The email
object that is sent via the signal has anattachmentsproperty,
but this contains a list of 3-tuples [(name, contents, content_type),],
not a list of file objects. In order to store the attachments against
a model as a FileField, you’ll need to convert the tuples back into
something that Django can deal with.fromdjango.core.files.uploadedfileimportSimpleUploadedFilefromdjango.dbimportmodelsfrominbound_email.signalsimportemail_receiveddefget_file(attachment):"""Convert email.attachment tuple into a SimpleUploadedFile."""name,content,content_type=attachmentreturnSimpleUploadedFile(name,content,content_type)classExample(models.Model):"""Example model that contains a FileField property."""file=models.FileField()defon_email_received(sender,**kwargs):"""Handle inbound emails."""email=kwargs.pop('email')forattachmentinemail.attachments:# we must convert attachment tuple into a file# before adding as the property.example=Example(file=get_file(attachment))example.save()TestsThere is a test django project,test_appthat is used to run the
tests.(django-inbound-email)django-inbound-email$pythonmanage.pytestConfigurationInstall the appAdd the app toINSTALLED_APPSAddINBOUND_EMAIL_PARSERsettingUpdate your provider configuration to point to app URL# the fully-qualified path to the provider's backend parserINBOUND_EMAIL_PARSER='inbound_email.backends.sendgrid.SendGridRequestParser'# if True (default=False) then log the contents of each inbound requestINBOUND_EMAIL_LOG_REQUESTS=True# if True (default=True) then always return HTTP status of 200 (may be required by provider)INBOUND_EMAIL_RESPONSE_200=True# add the app to Django's INSTALL_APPS settingINSTALLED_APPS=(# other apps# ...'inbound_email',)Mandrill FeaturesIf you wish to check the X-Mandrill-Signature header that Mandrill provides
in the requests, you will need to set theINBOUND_MANDRILL_AUTHENTICATION_KEYsetting to your Mandrill
authentication key. When the key is supplied Inbound Email will check the
signature supplied versus the one calculated from the request.If signatures don’t match, the system will send the signalemail_received_unacceptablewith the exception describing the problem.FeaturesThings it will do:Parse HTTP requests into EmailMultiAlternatives objectsPluggable backends (SendGrid, Mailgun and Mandrill currently supported)Handle character encodings properlyHandle attachments, including if they are too largeThings it (probably) won’t do:Handle email reply parsing - usehttps://github.com/zapier/email-reply-parser