{ "info": { "author": "Philip J Grabner, Cadit Health Inc", "author_email": "oss@cadit.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "License :: Public Domain", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Software Development" ], "description": "========\ngenemail\n========\n\n.. WARNING::\n\n 2013/10/23: although functional, genemail is still in beta, and the\n API may change. That said, it works quite well.\n\n`genemail` makes creating and sending templated email easier. The\nfollowing features are built-in:\n\n* **Automatic html-to-text conversion** so that all generated emails\n have both a plain-text and an HTML version. Note that if the auto-\n conversion is not sufficient, each version can have it's own\n template.\n\n* **Automatic inlining of CSS** for maximum backward compatibility\n with old and/or problematic email clients.\n\n* **Automatic attachment management** allows a common email template\n to specify default attachments; additional attachments can be added\n to individual emails.\n\n* **Support for DKIM email header generation** so that emails that\n are indeed not spam are less likely to be identified as such.\n\n* **Support for PGP email encryption** so that emails can contain\n sensitive information that should not be visible to the public.\n\n* **Preview data** allows templates to define sample data so that\n email previews can be generated with predefined data and/or dynamic\n data.\n\n* **Unit of test for generated emails** is made easier thanks to a\n sender mechanism that allows outbound emails to be trapped for\n analysis instead of being delivered and a unittest mixin class that\n provides the `assertEmailEqual` method that validates that the\n significant email headers, structure and content are the same.\n\n\nProject\n=======\n\n* Homepage: https://github.com/cadithealth/genemail\n* Bugs: https://github.com/cadithealth/genemail/issues\n\n\nTL;DR\n=====\n\nInstall:\n\n.. code-block:: bash\n\n $ pip install genemail\n\nGiven the following package file structure:\n\n::\n\n -- mypackage/\n `-- templates/\n `-- email/\n |-- logo.png\n |-- invite.html\n |-- invite.spec # if missing: defaults are used\n | Example content:\n | attachments:\n | - name: logo.png\n | value: !include-raw logo.png\n | cid: true\n `-- invite.text # if missing: auto-generated from .html\n\nUse genemail as follows:\n\n.. code-block:: python\n\n import genemail, templatealchemy as ta\n\n # configure a genemail manager that uses the local SMTP server\n # and uses mako templates from a python package named 'mypackage'\n manager = genemail.Manager(\n sender = genemail.SmtpSender(host='localhost', port='25'),\n provider = ta.Manager(\n source = 'pkg:mypackage:templates/email',\n renderer = 'mako'),\n modifier = genemail.DkimModifier(\n selector = 'selector._domainkey.example.com',\n key = '/path/to/private-rsa.key',\n )\n )\n\n # get an email template object\n eml = manager.newEmail('invite')\n\n # set some parameters that will be used by mako to render the\n # template\n eml['givenname'] = 'Joe'\n eml['surname'] = 'Schmoe'\n\n # add an ICS calendar invite\n eml.addAttachment(\n name = 'invite.ics',\n value = create_invite(...),\n contentType = 'text/calendar; name=invite.ics; method=PUBLISH')\n\n # and send the email\n eml.send()\n\n # the resulting email will:\n # - have two alternative formats (text/plain and text/html)\n # - have one top-level attachment (text/calendar)\n # - have one text/html related attachment (logo.png)\n # - be DKIM-signed\n\n\nOverview\n========\n\nTODO: add docs\n\n\nDKIM Signed Email\n=================\n\nTODO: add docs\n\n\nPer-Email Value Caching\n=======================\n\nWhen genemail renders a typical email with HTML, plain-text, subjects,\nand headers all being supplied by the same template, it by default\nevaluates the template many times with different ``genemail_format``\nvalues and different output renderings. This can be a problem, for\nexample, if the template calls out to dynamically generate content\nthat should only be evaluated once per email such as a pixel tracker.\n\nTo solve this, genemail inserts a default parameter named ``cache``\nwhich is an \"auto-caching dict\". The difference between a standard\n`dict` class and the `cache` parameter is that the `.get` method will\npopulate itself with the default value if the specified key does not\nexist. Furthermore, if the default value is a callable, it will first\ncall it (with no arguments) before caching it.\n\nThe following example makes use of a `makeUniqueUrl()` function that\ncan be used to track clicks in the email on a per-email basis. If it\ndid not use the `cache` object, `makeUniqueUrl()` would be called\nmultiple times per email.\n\n.. code-block:: mako\n\n
\n Please click on the link below:\n click me!\n
\n\nNote that this cache is a *per-email-instance* cache.\n\n\nEncrypted Email\n===============\n\nThe genemail ``pgp`` optional feature allows you to generate encrypted\noutbound email. It does this using the ``python-gnupg`` package, which\nin turn uses the ``gpg`` external command-line program. Genemail can\nboth encrypt and sign the emails, or only encrypt. Steps to generate\nencrypted email:\n\n1. First, create a GPG-home directory with all of the necessary\nkeys. For example:\n\n.. code-block:: bash\n\n # create the directory\n $ mkdir -p /path/to/gpghome\n $ chmod 700 /path/to/gpghome\n\n # for signing, a private key is needed. generate one:\n $ gpg --homedir /path/to/gpghome --gen-key\n\n # for encryption, the public key of every recipient of encrypted\n # emails is needed. do this for every recipient:\n $ gpg --homedir /path/to/gpghome --import /path/to/recipient/public.key\n\n2. Then, configure genemail to use the\n``genemail.modifier.PgpModifier`` modifier. For example:\n\n.. code-block:: python\n\n import genemail\n\n # configure a genemail manager using the modifier\n manager = genemail.Manager(\n # ...\n modifier = genemail.modifier.PgpModifier(\n sign = 'noreply@example.com',\n gpg_options = dict(gnupghome = '/path/to/gpghome'),\n ),\n # ...\n )\n\nPgpModifier takes the following parameters:\n\n* ``sign``: str, optional, default: null\n\n If specified, it is taken to be the ID or email address of the GPG\n key to use to sign outbound emails. In this case, either the\n passphrase must be empty, or you must be using a gpg-agent. The\n default is null, which disables signing.\n\n* ``add_key``: list(str), optional, default: 'sign-key'\n\n The `add_key` parameter specifies IDs or email addresses that should\n be added to the encryption list, but not to the recipient list.\n This is useful if a global 'backdoor' key is needed. It can also be\n set to ``'sign-key'`` (the default) which indicates that the signing\n key should be added (thus the sender can decrypt the sent\n messages). Set this to null to disable any addition. It can also be\n a list of values.\n\n* ``prune_keys``: bool, optional, default: true\n\n If truthy (the default), then the list of email addresses for whom\n the email is encrypted for is reduced to the set of recipients that\n have an exactly matching key. If too many addresses are pruned (this\n can happen if the gpg binary is smarter at matching an email address\n to a key), then this may need to be set to false -- but beware, if\n any address cannot be resolved to a key by gpg, then the entire\n encryption process fails, and the email is not sent.\n\n* ``prune_recipients``: bool, optional, default: false\n\n If truthy, then encrypted emails will only be sent to the list of\n addresses that were the result of a `prune_keys` pruning. If they\n are not pruned, the recipients will receive emails that they cannot\n read. This is by default false so that it is more obvious that some\n action needs to be taken (i.e. give the GPG-home directory the\n appropriate list of keys).\n\n* ``gpg_options``: dict, optional\n\n This parameter is a collection of parameters passed to gnupg. The\n only required parameter is `gnupghome`, which is the path to the\n GPG-home directory. All currently available parameters:\n\n * ``gnupghome``: str, optional, default: null\n * ``gpgbinary``: str, optional, default: 'gpg'\n * ``use_agent``: bool, optional, default: false\n * ``verbose``: bool, optional, default: false\n * ``keyring``: str, optional, default: null\n * ``secret_keyring``: str, optional, default: null\n * ``options``: list(str), optional, default: null\n\n\nUnit Testing\n============\n\nThe following example test code illustrates the recommended approach\nto do unit testing with genemail (note the use of the `pxml` library\nto compare HTML output):\n\n.. code-block:: python\n\n import unittest, pxml, genemail, genemail.testing\n\n class AppTest(genemail.testing.EmailTestMixin, pxml.XmlTestMixin, unittest.TestCase):\n\n def setUp(self):\n super(AppTest, self).setUp()\n self.sender = genemail.DebugSender()\n # the following is very subjective to how your app is built & used,\n # but the idea is to provide a different `sender` to genemail...\n self.app = App()\n self.app.genemail.sender = self.sender\n\n def test_email(self):\n\n # do something to cause an email to be sent\n self.app.send_an_email()\n\n # verify the sent email (which will have been trapped by self.sender)\n self.assertEqual(len(self.sender.emails), 1)\n self.assertEmailEqual(self.sender.emails[0], '''\\\n Content-Type: multipart/alternative; boundary=\"==BOUNDARY-MAIN==\"\n MIME-Version: 1.0\n Date: Fri, 13 Feb 2009 23:31:30 -0000\n To: test@example.com\n Message-ID: <1234567890@@genemail.example.com>\n From: noreply@example.com\n Subject: Test Subject\n\n --==BOUNDARY-MAIN==\n MIME-Version: 1.0\n Content-Type: text/plain; charset=\"us-ascii\"\n Content-Transfer-Encoding: 7bit\n\n Email text version.\n\n --==BOUNDARY-MAIN==\n Content-Type: multipart/related; boundary=\"==BOUNDARY-HTMLREL==\"\n MIME-Version: 1.0\n\n --==BOUNDARY-HTMLREL==\n MIME-Version: 1.0\n Content-Type: text/html; charset=\"us-ascii\"\n Content-Transfer-Encoding: 7bit\n\n Email html version.\n\n --==BOUNDARY-HTMLREL==\n Content-Type: image/png\n MIME-Version: 1.0\n Content-Transfer-Encoding: 7bit\n Content-Disposition: attachment\n Content-ID: