leela is an email sending service that allows you to create and customize your HTML emails easily, without coding. It allows embedded images and attachments.
It is built on:
- Python 3.4
- Django
- Postgres
- Mandrill
- RabbitMQ
leela has three main features:
- Admin site, that you'll use to manage your email configurations, and check out the generated emails. You will find it at /admin/ .
- Queue consumer, that will consume messages from a queue system to send configured emails.
- REST API, to make queries about sent emails.
The backend is mainly comprised of two types:
- EmailKind represents a configured type of email. These are created manually through the admin site.
- EmailEntry represents an actual email. It is generated by, and only by, the queue consumer. It is related to one EmailKind. Informally speaking, you could say that an EmailEntry is an instance of an EmailKind.
With leela there's no need to write any code to be able to send full featured HTML emails. The steps to configure a new email are:
Go to /admin/ => "Email kinds" => "Add email kind". There it is the form to create a new EmailKind. An EmailKind is identified by both its name and language (both together must be unique). You can change the allowed languages in the project settings.
Fill up the form with the desired templates. Plain template is mandatory, given that many email clients do not support HTML templates. The template and JSON fields use the CodeMirror editor to make it easier to write code there. The templates are rendered against the context
defined in the parameters, check below.
In order to embed images into the Template field you have to use the ContentID syntax with a placeholder: <img src="cid:my-troll-logo">
And then, with the file fields at the bottom, upload the desired image assigning the very same placeholder (in this case my-troll-logo). Real ContentIDs will be managed for you.
Keep in mind that you are defining the future interface for sending them (regarding leela it can be changed at any time, but could break your existing calls): When sending an email any default parameter that is not defined will be mandatory.
Now you can use both "Render test" to check the rendering of the templates in your browser, and "Send test" to actually send the email (yeah, will generate an EmailEntry).
To use your brand new email, connect to your queue system and send a JSON body message like this:
{
"name": "myproject_myemail_description",
"language": "es",
"sender": "[email protected]",
"recipients": ["[email protected]", "[email protected]", ...],
"reply_to": ["[email protected]", "[email protected]", ...],
"customer_id": "3838383",
"subject": "This is the email subject",
"context": {"first_name": "Troll", ...},
"send_at": 1434029573, # Unix timestamp, UTC
"check_url": "http://myservice.qdqmedia.com/canisend/4983/323",
"attachs": [
{"filename": "invoice.pdf",
"content_type": "application/pdf",
"content": "raw content of the file in encoded in base64"},
...],
"backend": "this-backend",
"meta_fields": {
"meta1": "metavalue1",
...
}
}
Remember that the encoding of the above message (the body of the queue message) has to be encoded in utf8. About the parameters:
sender
,recipients
,reply_to
andsubject
are treated with the defaults logic explained above.customer_id
is optional, in case you want to relate the email with a customer or anything else. Useful for future API queries.context
is optional, only add it if your template is going to use it.send_at
is optional, you can specify the UTC time at which you want your email to be sent. If not specified, will be send as soon as possible.check_url
is optional, you can set here an url which will be called by leela (GET) just before sending the email, to check if the email is still needed. The response is expected to be a JSON object with two boolean properties:{"allowed": ..., "delete": ...}
. Ifallowed
istrue
the email will be sent. Ifdelete
istrue
, andallowed
isfalse
it will be removed without sending.attachs
is optional, is an array of objects with the keysfilename
to set the attachment name,content_type
describing its MIME Type andcontent
with the raw content of the file itself. It is mandatory to encode the content in base64 to avoid the bytestring to break the JSON format.backend
is optional. You can specify the email backend to use to send the entry. For specifics on this, checkCUSTOM.md
.meta_fields
is optional. You can specify metadata information, but will only make sense if the backend chosen understands metadata.
The variables available in rendering context are the ones defined in the context
param object plus an object called meta
, which contains information about the EmailKind, with the attributes:
{
"id": 42, # Id of the EmailKind
"name": "...", # EmailKind name
"language": "es",
"template": "...",
"plain_template": "...",
"default_subject": "...",
"default_recipients": "...",
"default_reply_to": "...",
"default_context": "{}", # Do not access this var through meta.
"default_sender": "..."
}
It is possible to define Email Kind Fragments to avoid repeating content on emails (such as headers, footers, and so on). To do so the steps are:
- Create your EmailKindFragment using the admin interface.
- Select it in your EmailKind (using the fragments section).
- Once selected, the content of your fragment will be available in the email
context
when rendering, therefore you can use it in your EmailKind template using{{ fragments.fragment_name }}
.
NOTE: when you modify existing EmailKinds that use images to begin to use fragments you need to be careful if you care about history. If you move images from an EmailKind to an EmailKindFragment and you use the EmailKindFragment, the renders of emails sent before the modification will not find the images as those images were defined in the EmailKind. Thence, if the history is important for you, you will need to keep those images both in the EmailKind and the EmailKindFragment.
Some entry REST points are available to query about emails. The responses are in JSON format, and contain information about counters and pagination. All of them are grouped under the /api/ path:
/api/entries/
List all entries./api/entries/4/
Retrieves the entry withid = 4
./api/entries/?customer_id=06666666
Retrieves all the entries with thecustomer_id = 6666666
./api/entries/?include_kinds=solweb_contact
Retrieves all the entries from the email kindssolweb_contact
./api/attachs/
List all attachments./api/attachs/12/
Retrieves the attachment withid = 12
./api/legacyentries/3567883/
Retrieves the legacy entries previously stored in CDV with thecustomer_id = 3567883
.
The API is fully browsable, so you can just navigate to /api/
and check all the urls and responses visually.
The system can be configured to detect spam. In the leela/settings/custom.py
you can define the setting SPAM_CHECK
, a dictionary with EmailKind names as keys, and tuples with function paths as values. For example:
SPAM_CHECK = {
'my_lovely_email': ('custom.spamchecks.has_href',
'custom.spamchecks.above_remote_score')
}
In the example, all the entries from the EmailKinds of name my_lovely_email (in all its languages) will be filtered by the functions has_href
and above_remote_score
. These functions receive the EmailEntry that is about to be sent and should return either True or False. If any of them returns True, the entry will be classified as spam.
Spam checking should not be necessary in the majority of cases, where systems controlled by developers are the ones that send emails. In this case just don't add it to SPAM_CHECK
. If your email is sent as a result of a user form submission, you probably need it.
The project comes with no spam check functions by default, its your responsibility to build your own or plug other ones like SpamAssassin, Mollom, etc. given your needs and the nature of your email.
The project uses docker and docker-compose to set up a development environment. Everything is managed by a Makefile
to avoid having to type long commands. Available commands are:
build
: It's the first one to be called, will create all the docker images.runserver
: Starts the Django development server, forwarded to http://127.0.0.1:8005/ in your host.scheduler
: Starts the scheduler to listen to the queue system, waiting for schedule email calls.shell
: Starts a shell inside the app container. Remember to activate the virtualenv at/home/qdqmedia/leela
before managing the project.test
: Runs the tests suite.
Wherever you deploy it, you should take care of the following entry points. Check the details in the Procfile
file:
- Admin website served at /admin/ and API served at /api/
- Queue consumer job, configured in the settings of the project. It consumes messages from an AMPQ system in a blocking way. It is the Django command
$ python3 manage.py scheduler
. - Email sender job, continuously checks for new scheduled entries an sends them. It is the Django command
$ python3 manage.py send_emails
.
Checkout the Procfile
to know which processes you need to run.
To achieve some extensibility, the project does override some Django settings at run time. Because of the nature of Python running environments and Django settings, it is discouraged to run leela with multiple sender or scheduler processes. As an asynchronous system, sending latency should not bother you.
But if you need to scale the service, the recommended strategy is to deploy multiple independent leela systems and shard the load, for example by the domain of the email, that consume from different AMPQ queues.
For a complete customization guide, please check CUSTOM.md