Package: genemail
Warning2013/10/23: although functional, genemail is still in beta, and the
API may change. That said, it works quite well.genemailmakes creating and sending templated email easier. The
following features are built-in:Automatic html-to-text conversionso that all generated emails
have both a plain-text and an HTML version. Note that if the auto-
conversion is not sufficient, each version can have it’s own
template.Automatic inlining of CSSfor maximum backward compatibility
with old and/or problematic email clients.Automatic attachment managementallows a common email template
to specify default attachments; additional attachments can be added
to individual emails.Support for DKIM email header generationso that emails that
are indeed not spam are less likely to be identified as such.Support for PGP email encryptionso that emails can contain
sensitive information that should not be visible to the public.Preview dataallows templates to define sample data so that
email previews can be generated with predefined data and/or dynamic
data.Unit of test for generated emailsis made easier thanks to a
sender mechanism that allows outbound emails to be trapped for
analysis instead of being delivered and a unittest mixin class that
provides theassertEmailEqualmethod that validates that the
significant email headers, structure and content are the same.ProjectHomepage:https://github.com/cadithealth/genemailBugs:https://github.com/cadithealth/genemail/issuesTL;DRInstall:$pipinstallgenemailGiven the following package file structure:-- mypackage/
   `-- templates/
       `-- email/
           |-- logo.png
           |-- invite.html
           |-- invite.spec         # if missing: defaults are used
           |     Example content:
           |       attachments:
           |         - name:  logo.png
           |           value: !include-raw logo.png
           |           cid:   true
           `-- invite.text         # if missing: auto-generated from .htmlUse genemail as follows:importgenemail,templatealchemyasta# configure a genemail manager that uses the local SMTP server# and uses mako templates from a python package named 'mypackage'manager=genemail.Manager(sender=genemail.SmtpSender(host='localhost',port='25'),provider=ta.Manager(source='pkg:mypackage:templates/email',renderer='mako'),modifier=genemail.DkimModifier(selector='selector._domainkey.example.com',key='/path/to/private-rsa.key',))# get an email template objecteml=manager.newEmail('invite')# set some parameters that will be used by mako to render the# templateeml['givenname']='Joe'eml['surname']='Schmoe'# add an ICS calendar inviteeml.addAttachment(name='invite.ics',value=create_invite(...),contentType='text/calendar; name=invite.ics; method=PUBLISH')# and send the emaileml.send()# the resulting email will:#   - have two alternative formats (text/plain and text/html)#   - have one top-level attachment (text/calendar)#   - have one text/html related attachment (logo.png)#   - be DKIM-signedOverviewTODO: add docsDKIM Signed EmailTODO: add docsPer-Email Value CachingWhen genemail renders a typical email with HTML, plain-text, subjects,
and headers all being supplied by the same template, it by default
evaluates the template many times with differentgenemail_formatvalues and different output renderings. This can be a problem, for
example, if the template calls out to dynamically generate content
that should only be evaluated once per email such as a pixel tracker.To solve this, genemail inserts a default parameter namedcachewhich is an “auto-caching dict”. The difference between a standarddictclass and thecacheparameter is that the.getmethod will
populate itself with the default value if the specified key does not
exist. Furthermore, if the default value is a callable, it will first
call it (with no arguments) before caching it.The following example makes use of amakeUniqueUrl()function that
can be used to track clicks in the email on a per-email basis. If it
did not use thecacheobject,makeUniqueUrl()would be called
multiple times per email.<p>
 Please click on the link below:
 <a href="${cache.get('myCacheKey',lambda:makeUniqueUrl())}">click me!</a>
</p>Note that this cache is aper-email-instancecache.Encrypted EmailThe genemailpgpoptional feature allows you to generate encrypted
outbound email. It does this using thepython-gnupgpackage, which
in turn uses thegpgexternal command-line program. Genemail can
both encrypt and sign the emails, or only encrypt. Steps to generate
encrypted email:1. First, create a GPG-home directory with all of the necessary
keys. For example:# create the directory$mkdir-p/path/to/gpghome$chmod700/path/to/gpghome# for signing, a private key is needed. generate one:$gpg--homedir/path/to/gpghome--gen-key# for encryption, the public key of every recipient of encrypted
# emails is needed. do this for every recipient:$gpg--homedir/path/to/gpghome--import/path/to/recipient/public.key2. Then, configure genemail to use thegenemail.modifier.PgpModifiermodifier. For example:importgenemail# configure a genemail manager using the modifiermanager=genemail.Manager(# ...modifier=genemail.modifier.PgpModifier(sign='noreply@example.com',gpg_options=dict(gnupghome='/path/to/gpghome'),),# ...)PgpModifier takes the following parameters:sign: str, optional, default: nullIf specified, it is taken to be the ID or email address of the GPG
key to use to sign outbound emails. In this case, either the
passphrase must be empty, or you must be using a gpg-agent. The
default is null, which disables signing.add_key: list(str), optional, default: ‘sign-key’Theadd_keyparameter specifies IDs or email addresses that should
be added to the encryption list, but not to the recipient list.
This is useful if a global ‘backdoor’ key is needed. It can also be
set to'sign-key'(the default) which indicates that the signing
key should be added (thus the sender can decrypt the sent
messages). Set this to null to disable any addition. It can also be
a list of values.prune_keys: bool, optional, default: trueIf truthy (the default), then the list of email addresses for whom
the email is encrypted for is reduced to the set of recipients that
have an exactly matching key. If too many addresses are pruned (this
can happen if the gpg binary is smarter at matching an email address
to a key), then this may need to be set to false – but beware, if
any address cannot be resolved to a key by gpg, then the entire
encryption process fails, and the email is not sent.prune_recipients: bool, optional, default: falseIf truthy, then encrypted emails will only be sent to the list of
addresses that were the result of aprune_keyspruning. If they
are not pruned, the recipients will receive emails that they cannot
read. This is by default false so that it is more obvious that some
action needs to be taken (i.e. give the GPG-home directory the
appropriate list of keys).gpg_options: dict, optionalThis parameter is a collection of parameters passed to gnupg. The
only required parameter isgnupghome, which is the path to the
GPG-home directory. All currently available parameters:gnupghome: str, optional, default: nullgpgbinary: str, optional, default: ‘gpg’use_agent: bool, optional, default: falseverbose: bool, optional, default: falsekeyring: str, optional, default: nullsecret_keyring: str, optional, default: nulloptions: list(str), optional, default: nullUnit TestingThe following example test code illustrates the recommended approach
to do unit testing with genemail (note the use of thepxmllibrary
to compare HTML output):importunittest,pxml,genemail,genemail.testingclassAppTest(genemail.testing.EmailTestMixin,pxml.XmlTestMixin,unittest.TestCase):defsetUp(self):super(AppTest,self).setUp()self.sender=genemail.DebugSender()# the following is very subjective to how your app is built & used,# but the idea is to provide a different `sender` to genemail...self.app=App()self.app.genemail.sender=self.senderdeftest_email(self):# do something to cause an email to be sentself.app.send_an_email()# verify the sent email (which will have been trapped by self.sender)self.assertEqual(len(self.sender.emails),1)self.assertEmailEqual(self.sender.emails[0],'''\Content-Type: multipart/alternative; boundary="==BOUNDARY-MAIN=="
MIME-Version: 1.0
Date: Fri, 13 Feb 2009 23:31:30 -0000
To: test@example.com
Message-ID: <1234567890@@genemail.example.com>
From: noreply@example.com
Subject: Test Subject

--==BOUNDARY-MAIN==
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit

Email text version.

--==BOUNDARY-MAIN==
Content-Type: multipart/related; boundary="==BOUNDARY-HTMLREL=="
MIME-Version: 1.0

--==BOUNDARY-HTMLREL==
MIME-Version: 1.0
Content-Type: text/html; charset="us-ascii"
Content-Transfer-Encoding: 7bit

<html><body>Email html version.</body></html>

--==BOUNDARY-HTMLREL==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment
Content-ID: <logo.png>

PNG.BINARY.DATA...
--==BOUNDARY-HTMLREL==--
--==BOUNDARY-MAIN==--
''')