├── setup.cfg ├── templates ├── change_email.txt ├── confirm_account.txt └── reset_password.txt ├── .gitignore ├── CHANGELOG ├── LICENSE ├── setup.py ├── test_ses_mailer.py ├── README.md └── ses_mailer.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /templates/change_email.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | To confirm your new email address click on the following link: 4 | 5 | {{ url }} 6 | 7 | Thank you. 8 | 9 | Note: replies to this email address are not monitored. -------------------------------------------------------------------------------- /templates/confirm_account.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | Welcome to {{ site_name }}! 4 | 5 | To confirm your account please click on the following link: 6 | 7 | {{ url }} 8 | 9 | Thank you. 10 | 11 | Note: replies to this email address are not monitored. -------------------------------------------------------------------------------- /templates/reset_password.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | To reset your password click on the following link: 4 | 5 | {{ url }} 6 | 7 | If you have not requested a password reset simply ignore this message. 8 | 9 | Thank you 10 | 11 | Note: replies to this email address are not monitored. 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__* 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | MANIFEST 17 | .cache 18 | 19 | # PyCharm 20 | .idea/* 21 | .idea/libraries/sass_stdlib.xml 22 | 23 | # Distribution 24 | dist/* 25 | 26 | # Mac stuff 27 | .DS_Store 28 | 29 | docs/_build* 30 | 31 | test_config.py -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2 | - 0.13.0 3 | - Merge pull request that added AWS region. By default 'us-east-1' 4 | 5 | [0.7 - 0.12] 6 | I forgot what happened. :( check the commit history :) 7 | 8 | - 0.6: 9 | - Added params: sender and reply_to to manually include the sender and reply to 10 | 11 | - 0.5 12 | - Added prop __init__ :default_context: and conf SES_MAILER_TEMPLATE_DEFAULT_CONTEXT 13 | to add default context 14 | 15 | - 0.4 16 | - added TemplateMail() to use template file (jinja) 17 | 18 | - 0.3 19 | - rename is_email_valid to is_valid_email 20 | 21 | - 0.2 22 | - Added is_email_valid() to valid if email is valid or not 23 | 24 | - Version 0.1.0 25 | - First version -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mardix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | SESMailer 3 | """ 4 | 5 | from setuptools import setup, find_packages 6 | 7 | import ses_mailer 8 | 9 | PACKAGE = ses_mailer 10 | 11 | setup( 12 | name=PACKAGE.__NAME__, 13 | version=PACKAGE.__version__, 14 | license=PACKAGE.__license__, 15 | author=PACKAGE.__author__, 16 | author_email='mardix@github.com', 17 | description="A simple module to send email via AWS SES", 18 | long_description=PACKAGE.__doc__, 19 | url='http://github.com/mardix/ses-mailer/', 20 | download_url='http://github.com/mardix/ses-mailer/tarball/master', 21 | py_modules=['ses_mailer'], 22 | include_package_data=True, 23 | install_requires=[ 24 | "boto", 25 | "jinja2" 26 | ], 27 | 28 | keywords=['email', 29 | 'flask', 30 | 'aws ses', 31 | 'amazon', 32 | 'ses', 33 | 'mailer', 34 | 'jinja2', 35 | 'template email'], 36 | platforms='any', 37 | classifiers=[ 38 | 'Environment :: Web Environment', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: BSD License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 2', 44 | 'Programming Language :: Python :: 2.6', 45 | 'Programming Language :: Python :: 2.7', 46 | 'Programming Language :: Python :: 3', 47 | 'Programming Language :: Python :: 3.3', 48 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 49 | 'Topic :: Software Development :: Libraries :: Python Modules' 50 | ], 51 | packages=find_packages(exclude=["test_config.py"]), 52 | zip_safe=False 53 | ) 54 | -------------------------------------------------------------------------------- /test_ses_mailer.py: -------------------------------------------------------------------------------- 1 | """ 2 | SES Mailer Test 3 | 4 | 5 | Create a file test_config.py and add the following 6 | 7 | AWS_ACCESS_KEY_ID = "" 8 | AWS_SECRET_ACCESS_KEY = "" 9 | AWS_REGION = "" 10 | EMAIL_SENDER = "" 11 | EMAIL_RECIPIENT = "" 12 | 13 | """ 14 | 15 | import pytest 16 | from jinja2 import Template as jinjaTemplate 17 | from ses_mailer import Mail, Template 18 | import test_config 19 | 20 | 21 | templateMap = { 22 | "test": """ 23 | {% block subject %}I'm subject{% endblock %} 24 | {% block body %}How are you {{name}}?{% endblock %} 25 | """, 26 | "contact": "Contact us at {{email}}", 27 | "welcome": """ 28 | {% block subject %}I'm subject{% endblock %} 29 | {% block body %}How are you {{name}}?{% endblock %} 30 | """, 31 | "no_blocks": "Contact us at {{email}}", 32 | "no_subject": """ 33 | {% block body %}How are you {{name}}?{% endblock %} 34 | """, 35 | "no_body": """ 36 | {% block subject %}I'm subject{% endblock %} 37 | """ 38 | } 39 | template = Template(templateMap) 40 | 41 | def test_template_is_jinja(): 42 | assert isinstance(template._get_template("test"), jinjaTemplate) 43 | 44 | def test_template_block(): 45 | assert isinstance(template.render_blocks("test"), dict) 46 | 47 | def test_template_rendered_block(): 48 | blocks = template.render_blocks("test") 49 | assert "subject" in blocks 50 | assert "body" in blocks 51 | assert "largo" not in "blocks" 52 | 53 | def test_template_render(): 54 | name = "Jones" 55 | line = "How are you %s?" % name 56 | assert line == template.render("test", "body", name=name) 57 | 58 | ##---- 59 | 60 | mail = Mail(aws_secret_access_key=test_config.AWS_SECRET_ACCESS_KEY, 61 | aws_access_key_id=test_config.AWS_ACCESS_KEY_ID, 62 | region=test_config.AWS_REGION, 63 | sender=test_config.EMAIL_SENDER, 64 | template=templateMap) 65 | 66 | def test_mail_get_sender(): 67 | assert isinstance(mail._get_sender(("Name", "name@yahoo.com")), tuple) 68 | assert "Name " == mail._get_sender(("Name", "name@yahoo.com"))[0] 69 | 70 | def test_mail_send(): 71 | r = mail.send(test_config.EMAIL_RECIPIENT, subject="Test", body="TEST BODY") 72 | assert r is not None 73 | 74 | def test_mail_send_change_reply_to(): 75 | r = mail.send(test_config.EMAIL_RECIPIENT, subject="Test With Reply To", reply_to="nola@nola.com", body="TEST BODY - reply to nola@nola.com") 76 | assert r is not None 77 | 78 | def test_mail_send_template_error(): 79 | with pytest.raises(AttributeError): 80 | mail.send_template("no_blocks", to=test_config.EMAIL_RECIPIENT) 81 | mail.send_template("no_subject", to=test_config.EMAIL_RECIPIENT) 82 | mail.send_template("no_body", to=test_config.EMAIL_RECIPIENT) 83 | 84 | 85 | def test_send_template_welcome(): 86 | r = mail.send_template("welcome", to=test_config.EMAIL_RECIPIENT, name="Mardix") 87 | assert r is not None 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SES Mailer 2 | 3 | A simple module to send email via AWS SES 4 | 5 | You can use it as standalone or with Flask 6 | 7 | Requirements: 8 | 9 | - AWS Credentials 10 | - boto 11 | - Jinja 12 | 13 | --- 14 | 15 | ## Install 16 | 17 | pip install ses-mailer 18 | 19 | ## Usage 20 | 21 | ### Setup 22 | 23 | from ses_mailer import Mail 24 | 25 | mail = Mail(aws_access_key_id="####", 26 | aws_secret_access_key="####", 27 | region="us-east-1", 28 | sender="me@myemail.com", 29 | reply_to="me@email.com", 30 | template="./email-templates") 31 | 32 | #### Send Basic Email 33 | 34 | mail.send(to="you@email.com", 35 | subject="My Email subject", 36 | body="My message body") 37 | 38 | #### Send Template Email 39 | 40 | mail.send_template(template="welcome.txt", 41 | to="you@email.com", 42 | name="Ricky Roze", 43 | user_id=12739) 44 | 45 | 46 | ##API 47 | 48 | **Mail.\__init__** 49 | 50 | - aws_access_key_id 51 | - aws_secret_access_key 52 | - sender 53 | - reply_to 54 | - template 55 | 56 | **Mail.send** 57 | 58 | - to 59 | - subject 60 | - body 61 | - reply_to 62 | 63 | **Mail.send_template** 64 | 65 | - template 66 | - to 67 | - reply_to 68 | - **context 69 | 70 | 71 | ### Initiate with Flask 72 | 73 | from flask import Flask 74 | from ses_mailer import Mail 75 | 76 | app = Flask(__name__) 77 | 78 | mail = Mail() 79 | mail.init_app(app) 80 | 81 | 82 | ## Templates 83 | 84 | You can use pre-made templates to send email 85 | 86 | The template must be a Jinja template, containing at least the following blocks: 87 | 88 | - subject 89 | - body 90 | 91 | 92 | welcome.txt 93 | 94 | {% block subject %} 95 | Welcome {{name}} to our site 96 | {% endblock %} 97 | 98 | {% block body %} 99 | Dear {{name}} this is the content of the message 100 | 101 | Thank you very much for your visiting us 102 | {% endblock %} 103 | 104 | 105 | ### File Templates: 106 | 107 | Place you template files inside of a directory, let's say: `email-templates` 108 | 109 | Inside of `email-templates` we have the following files: 110 | 111 | /email-templates 112 | | 113 | |_ welcome.txt 114 | | 115 | |_ lost-password.txt 116 | 117 | 118 | mail = Mail(aws_access_key_id="####", 119 | aws_secret_access_key="####", 120 | sender="me@myemail.com", 121 | reply_to="me@email.com", 122 | template="./email-templates") 123 | 124 | ### Dictionary based templates 125 | 126 | If you don't want to create files, you can dictionary based templates 127 | 128 | templates = { 129 | "welcome.txt": """ 130 | {% block subject %}I'm subject{% endblock %} 131 | {% block body %}How are you {{name}}?{% endblock %} 132 | """, 133 | "lost-password.txt": """ 134 | {% block subject %}Lost Password{% endblock %} 135 | {% block body %}Hello {{ name }}. 136 | Here's your new password: {{ new_password }} 137 | {% endblock %} 138 | """, 139 | } 140 | 141 | mail = Mail(aws_access_key_id="####", 142 | aws_secret_access_key="####", 143 | sender="me@myemail.com", 144 | reply_to="me@email.com", 145 | template=templates) 146 | 147 | To send the email for either files or dictionary based templates: 148 | 149 | new_password = "mynewpassword" 150 | mail.send_template("lost-password.txt", to="x@y.com", name="Lolo", new_password=new_password) 151 | 152 | ### Config For Flask 153 | 154 | SES-Mailer is configured through the standard Flask config API. 155 | These are the available options: 156 | 157 | **SES_AWS_ACCESS_KEY** : Your AWS access key id 158 | 159 | **SES_AWS_SECRET_KEY**: Your AWS secred key 160 | 161 | **SES_REGION**: AWS region of the SES 162 | 163 | **SES_SENDER**: The sender email address as string. 164 | 165 | **SES_REPLY_TO**: The reply to address 166 | 167 | **SES_TEMPLATE**: (mixed) directory or dict of template to use as template 168 | 169 | **SES_TEMPLATE_CONTEXT**: A dict of default data to be passed by default 170 | 171 | SES_AWS_ACCESS_KEY = "" 172 | SES_AWS_SECRET_KEY = "" 173 | SES_REGION = "" 174 | SES_SENDER = "" 175 | SES_REPLY_TO = "" 176 | SES_TEMPLATE = None 177 | SES_TEMPLATE_CONTEXT = {} 178 | 179 | 180 | --- 181 | 182 | (c) 2015 Mardix 183 | 184 | -------------------------------------------------------------------------------- /ses_mailer.py: -------------------------------------------------------------------------------- 1 | """ 2 | SES-Mailer 3 | 4 | A simple module to send email via AWS SES 5 | 6 | It also allow you to use templates to send email 7 | 8 | """ 9 | import os 10 | import re 11 | try: 12 | import boto 13 | import boto.ses 14 | except ImportError as ex: 15 | print("Boto is missing. pip --install boto") 16 | try: 17 | from jinja2 import Environment, FileSystemLoader, DictLoader 18 | except ImportError as ex: 19 | print("Jinja2 is missing. pip --install jinja2") 20 | 21 | 22 | __NAME__ = "SES-Mailer" 23 | __version__ = "0.13.0" 24 | __license__ = "MIT" 25 | __author__ = "Mardix" 26 | __copyright__ = "(c) 2015 Mardix" 27 | 28 | 29 | def is_valid_email(email): 30 | """ 31 | Check if email is valid 32 | """ 33 | pattern = '[\w\.-]+@[\w\.-]+[.]\w+' 34 | return re.match(pattern, email) 35 | 36 | 37 | class Template(object): 38 | env = None 39 | chached_templates = {} 40 | 41 | def __init__(self, template): 42 | """ 43 | :param template: (string | dict) - The directory or dict map of templates 44 | - as string: A directory 45 | - as dict: {'index.html': 'source here'} 46 | """ 47 | loader = None 48 | if template: 49 | if isinstance(template, dict): 50 | loader = DictLoader(template) 51 | elif os.path.isdir(template): 52 | loader = FileSystemLoader(template) 53 | 54 | if loader: 55 | self.env = Environment(loader=loader) 56 | 57 | def _get_template(self, template_name): 58 | """ 59 | Retrieve the cached version of the template 60 | """ 61 | if template_name not in self.chached_templates: 62 | self.chached_templates[template_name] = self.env.get_template(template_name) 63 | return self.chached_templates[template_name] 64 | 65 | 66 | def render_blocks(self, template_name, **context): 67 | """ 68 | To render all the blocks 69 | :param template_name: The template file name 70 | :param context: **kwargs context to render 71 | :retuns dict: of all the blocks with block_name as key 72 | """ 73 | blocks = {} 74 | template = self._get_template(template_name) 75 | for block in template.blocks: 76 | blocks[block] = self._render_context(template, 77 | template.blocks[block], 78 | **context) 79 | return blocks 80 | 81 | def render(self, template_name, block, **context): 82 | """ 83 | TO render a block in the template 84 | :param template_name: the template file name 85 | :param block: the name of the block within {% block $block_name %} 86 | :param context: **kwargs context to render 87 | :returns string: of rendered content 88 | """ 89 | template = self._get_template(template_name) 90 | return self._render_context(template, 91 | template.blocks[block], 92 | **context) 93 | 94 | def _render_context(self, template, block, **context): 95 | """ 96 | Render a block to a string with its context 97 | """ 98 | return u''.join(block(template.new_context(context))) 99 | 100 | 101 | class Mail(object): 102 | """ 103 | To send basic email 104 | """ 105 | ses = None 106 | sender = None 107 | reply_to = None 108 | template = None 109 | template_context = {} 110 | 111 | def __init__(self, 112 | aws_access_key_id=None, 113 | aws_secret_access_key=None, 114 | region="us-east-1", 115 | sender=None, 116 | reply_to=None, 117 | template=None, 118 | template_context={}, 119 | app=None): 120 | """ 121 | Setup the mail 122 | 123 | """ 124 | if app: 125 | self.init_app(app) 126 | else: 127 | if region: 128 | self.ses = boto.ses.connect_to_region(region, 129 | aws_access_key_id=aws_access_key_id, 130 | aws_secret_access_key=aws_secret_access_key) 131 | else: 132 | self.ses = boto.connect_ses(aws_access_key_id=aws_access_key_id, 133 | aws_secret_access_key=aws_secret_access_key) 134 | 135 | self.sender = sender 136 | self.reply_to = reply_to or self.sender 137 | 138 | if template: 139 | self.template = Template(template=template) 140 | if template_context: 141 | self.template_context = template_context 142 | 143 | def init_app(self, app): 144 | """ 145 | For Flask using the app config 146 | """ 147 | self.__init__(aws_access_key_id=app.config.get("SES_AWS_ACCESS_KEY"), 148 | aws_secret_access_key=app.config.get("SES_AWS_SECRET_KEY"), 149 | region=app.config.get("SES_REGION", "us-east-1"), 150 | sender=app.config.get("SES_SENDER", None), 151 | reply_to=app.config.get("SES_REPLY_TO", None), 152 | template=app.config.get("SES_TEMPLATE", None), 153 | template_context=app.config.get("SES_TEMPLATE_CONTEXT", {}) 154 | ) 155 | 156 | 157 | def send(self, to, subject, body, reply_to=None, **kwargs): 158 | """ 159 | Send email via AWS SES. 160 | :returns string: message id 161 | 162 | *** 163 | 164 | Composes an email message based on input data, and then immediately 165 | queues the message for sending. 166 | 167 | :type to: list of strings or string 168 | :param to: The To: field(s) of the message. 169 | 170 | :type subject: string 171 | :param subject: The subject of the message: A short summary of the 172 | content, which will appear in the recipient's inbox. 173 | 174 | :type body: string 175 | :param body: The message body. 176 | 177 | :sender: email address of the sender. String or typle(name, email) 178 | :reply_to: email to reply to 179 | 180 | **kwargs: 181 | 182 | :type cc_addresses: list of strings or string 183 | :param cc_addresses: The CC: field(s) of the message. 184 | 185 | :type bcc_addresses: list of strings or string 186 | :param bcc_addresses: The BCC: field(s) of the message. 187 | 188 | :type format: string 189 | :param format: The format of the message's body, must be either "text" 190 | or "html". 191 | 192 | :type return_path: string 193 | :param return_path: The email address to which bounce notifications are 194 | to be forwarded. If the message cannot be delivered 195 | to the recipient, then an error message will be 196 | returned from the recipient's ISP; this message 197 | will then be forwarded to the email address 198 | specified by the ReturnPath parameter. 199 | 200 | :type text_body: string 201 | :param text_body: The text body to send with this email. 202 | 203 | :type html_body: string 204 | :param html_body: The html body to send with this email. 205 | 206 | """ 207 | if not self.sender: 208 | raise AttributeError("Sender email 'sender' or 'source' is not provided") 209 | 210 | kwargs["to_addresses"] = to 211 | kwargs["subject"] = subject 212 | kwargs["body"] = body 213 | kwargs["source"] = self._get_sender(self.sender)[0] 214 | kwargs["reply_addresses"] = self._get_sender(reply_to or self.reply_to)[2] 215 | 216 | response = self.ses.send_email(**kwargs) 217 | return response["SendEmailResponse"]["SendEmailResult"]["MessageId"] 218 | 219 | def send_template(self, template, to, reply_to=None, **context): 220 | """ 221 | Send email from template 222 | """ 223 | mail_data = self.parse_template(template, **context) 224 | subject = mail_data["subject"] 225 | body = mail_data["body"] 226 | del(mail_data["subject"]) 227 | del(mail_data["body"]) 228 | 229 | return self.send(to=to, 230 | subject=subject, 231 | body=body, 232 | reply_to=reply_to, 233 | **mail_data) 234 | 235 | def parse_template(self, template, **context): 236 | """ 237 | To parse a template and return all the blocks 238 | """ 239 | required_blocks = ["subject", "body"] 240 | optional_blocks = ["text_body", "html_body", "return_path", "format"] 241 | 242 | if self.template_context: 243 | context = dict(self.template_context.items() + context.items()) 244 | blocks = self.template.render_blocks(template, **context) 245 | 246 | for rb in required_blocks: 247 | if rb not in blocks: 248 | raise AttributeError("Template error: block '%s' is missing from '%s'" % (rb, template)) 249 | 250 | mail_params = { 251 | "subject": blocks["subject"].strip(), 252 | "body": blocks["body"] 253 | } 254 | for ob in optional_blocks: 255 | if ob in blocks: 256 | if ob == "format" and mail_params[ob].lower() not in ["html", "text"]: 257 | continue 258 | mail_params[ob] = blocks[ob] 259 | return mail_params 260 | 261 | 262 | def _get_sender(self, sender): 263 | """ 264 | Return a tuple of 3 elements 265 | 0: the email signature "Me " 266 | 1: the name "Me" 267 | 2: the email address "email@me.com" 268 | 269 | if sender is an email string, all 3 elements will be the email address 270 | """ 271 | if isinstance(sender, tuple): 272 | return "%s <%s>" % sender, sender[0], sender[1] 273 | else: 274 | return sender, sender, sender 275 | 276 | --------------------------------------------------------------------------------