├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── codecov.yml ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── sgbackend ├── __init__.py ├── mail.py └── version.py └── tests ├── test_api_key.py ├── test_mail.py └── test_send_messages.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = sgbackend, tests 4 | omit = site-packages 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pypirc 2 | build 3 | dist 4 | sendgrid_django.egg-info 5 | .pyc 6 | .eggs 7 | venv 8 | .idea 9 | .DS_Store 10 | .python-version 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | env/ 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *,cover 55 | .hypothesis/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # IPython Notebook 79 | .ipynb_checkpoints 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | venv/ 89 | ENV/ 90 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | - "3.8" 10 | env: 11 | - DJANGO_VERSION=1.11 12 | - DJANGO_VERSION=2.0 13 | - DJANGO_VERSION=2.1 14 | - DJANGO_VERSION=2.2 15 | - DJANGO_VERSION=3.0rc1 16 | jobs: 17 | exclude: 18 | - python: "3.8" 19 | env: DJANGO_VERSION=1.11 20 | - python: "2.7" 21 | env: DJANGO_VERSION=2.0 22 | - python: "3.8" 23 | env: DJANGO_VERSION=2.0 24 | - python: "2.7" 25 | env: DJANGO_VERSION=2.1 26 | - python: "3.4" 27 | env: DJANGO_VERSION=2.1 28 | - python: "3.8" 29 | env: DJANGO_VERSION=2.1 30 | - python: "2.7" 31 | env: DJANGO_VERSION=2.2 32 | - python: "3.4" 33 | env: DJANGO_VERSION=2.2 34 | - python: "2.7" 35 | env: DJANGO_VERSION=3.0rc1 36 | - python: "3.4" 37 | env: DJANGO_VERSION=3.0rc1 38 | - python: "3.5" 39 | env: DJANGO_VERSION=3.0rc1 40 | install: 41 | - pip install -e . 42 | - pip install -q Django~=$DJANGO_VERSION 43 | - pip install -rrequirements-dev.txt 44 | - pip install codecov 45 | script: 46 | - make coverage 47 | after_success: 48 | - codecov 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | coverage: 2 | py.test --cov sgbackend --cov-report term-missing tests/ 3 | 4 | test: 5 | py.test 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SendGrid-django 2 | =============== 3 | 4 | .. image:: https://travis-ci.org/elbuo8/sendgrid-django.svg?branch=master 5 | :target: https://travis-ci.org/elbuo8/sendgrid-django 6 | :alt: Travis CI 7 | .. image:: https://codecov.io/github/elbuo8/sendgrid-django/coverage.svg?branch=master 8 | :target: https://codecov.io/github/elbuo8/sendgrid-django 9 | :alt: codecov.io 10 | 11 | Simple django backend to send email using SendGrid's Web API. 12 | 13 | Installation 14 | ------------ 15 | 16 | Install the backend from PyPI: 17 | 18 | .. code:: bash 19 | 20 | pip install sendgrid-django 21 | 22 | Add the following to your project's **settings.py**: 23 | 24 | .. code:: python 25 | 26 | EMAIL_BACKEND = "sgbackend.SendGridBackend" 27 | SENDGRID_API_KEY = "Your SendGrid API Key" 28 | 29 | **Done!** 30 | 31 | Example 32 | ------- 33 | 34 | .. code:: python 35 | 36 | 37 | from django.core.mail import send_mail 38 | from django.core.mail import EmailMultiAlternatives 39 | 40 | send_mail("Your Subject", "This is a simple text email body.", 41 | "Yamil Asusta ", ["yamil@sendgrid.com"]) 42 | 43 | # or 44 | mail = EmailMultiAlternatives( 45 | subject="Your Subject", 46 | body="This is a simple text email body.", 47 | from_email="Yamil Asusta ", 48 | to=["yamil@sendgrid.com"], 49 | headers={"Reply-To": "support@sendgrid.com"} 50 | ) 51 | # Add template 52 | mail.template_id = 'YOUR TEMPLATE ID FROM SENDGRID ADMIN' 53 | 54 | # Replace substitutions in sendgrid template 55 | mail.substitutions = {'%username%': 'elbuo8'} 56 | 57 | # Attach file 58 | with open('somefilename.pdf', 'rb') as file: 59 | mail.attachments = [ 60 | ('somefilename.pdf', file.read(), 'application/pdf') 61 | ] 62 | 63 | # Add categories 64 | mail.categories = [ 65 | 'work', 66 | 'urgent', 67 | ] 68 | 69 | mail.attach_alternative( 70 | "

This is a simple HTML email body

", "text/html" 71 | ) 72 | 73 | mail.send() 74 | 75 | To create an instance of a SendGridBackend with an API key other than that provided in settings, pass `api_key` to the constructor 76 | 77 | .. code::python 78 | 79 | from sgbackend import SendGridBackend 80 | from django.core.mail import send_mail 81 | 82 | connection = SendGridBackend(api_key='your key') 83 | 84 | send_mail(, connection=connection) 85 | 86 | 87 | License 88 | ------- 89 | MIT 90 | 91 | 92 | Enjoy :) 93 | 94 | 95 | Development 96 | ----------- 97 | 98 | Install dependencies:: 99 | `pip install -r requirements-dev.txt` 100 | 101 | Run the tests with coverage:: 102 | `pytest --cov=sgbackend` 103 | 104 | If you see the error "No module named sgbackend", run:: 105 | `pip install -e .` -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "95..100" -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | pytest>=3.6 3 | pytest-cov 4 | pytest-django 5 | Django 6 | sendgrid>=3.6.5,<4 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | __version__ = None 4 | with open('sgbackend/version.py') as f: 5 | exec(f.read()) 6 | 7 | setup( 8 | name='sendgrid-django', 9 | version=str(__version__), 10 | author='Yamil Asusta', 11 | author_email='yamil@sendgrid.com', 12 | url='https://github.com/elbuo8/sendgrid-django', 13 | packages=find_packages(), 14 | license='MIT', 15 | description='SendGrid Backend for Django', 16 | long_description=open('./README.rst').read(), 17 | install_requires=[ 18 | "python_http_client >= 2.1.*, <2.3", 19 | "sendgrid >= 3.5, <4", 20 | ], 21 | classifiers=[ 22 | "Development Status :: 5 - Production/Stable", 23 | "Environment :: Web Environment", 24 | "Framework :: Django", 25 | "Intended Audience :: Developers", 26 | "License :: OSI Approved :: MIT License", 27 | "Natural Language :: English", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 2", 31 | "Programming Language :: Python :: 2.7", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Programming Language :: Python :: 3.6", 36 | "Topic :: Communications :: Email", 37 | "Topic :: Software Development :: Libraries :: Python Modules", 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /sgbackend/__init__.py: -------------------------------------------------------------------------------- 1 | from .mail import SendGridBackend # pragma: no cover 2 | from .version import __version__ # pragma: no cover 3 | -------------------------------------------------------------------------------- /sgbackend/mail.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | 3 | import base64 4 | import sys 5 | from email.mime.base import MIMEBase 6 | 7 | try: 8 | from urllib.error import HTTPError # pragma: no cover 9 | except ImportError: # pragma: no cover 10 | from urllib2 import HTTPError # pragma: no cover 11 | 12 | try: 13 | import rfc822 14 | except ImportError: 15 | import email.utils as rfc822 16 | 17 | from django.conf import settings 18 | from django.core.exceptions import ImproperlyConfigured 19 | from django.core.mail import EmailMultiAlternatives 20 | from django.core.mail.backends.base import BaseEmailBackend 21 | 22 | import sendgrid 23 | from sendgrid.helpers.mail import ( 24 | Attachment, 25 | Category, 26 | Content, 27 | CustomArg, 28 | Email, 29 | Mail, 30 | Personalization, 31 | Substitution, 32 | ) 33 | 34 | 35 | class SendGridBackend(BaseEmailBackend): 36 | ''' 37 | SendGrid Web API Backend 38 | ''' 39 | def __init__(self, fail_silently=False, **kwargs): 40 | super(SendGridBackend, self).__init__( 41 | fail_silently=fail_silently, **kwargs) 42 | if 'api_key' in kwargs: 43 | self.api_key = kwargs['api_key'] 44 | else: 45 | self.api_key = getattr(settings, "SENDGRID_API_KEY", None) 46 | 47 | if not self.api_key: 48 | raise ImproperlyConfigured(''' 49 | SENDGRID_API_KEY must be declared in settings.py''') 50 | 51 | self.sg = sendgrid.SendGridAPIClient(apikey=self.api_key) 52 | self.version = 'sendgrid/{0};django'.format(__version__) 53 | self.sg.client.request_headers['User-agent'] = self.version 54 | 55 | def send_messages(self, emails): 56 | ''' 57 | Comments 58 | ''' 59 | if not emails: 60 | return 61 | 62 | count = 0 63 | for email in emails: 64 | mail = self._build_sg_mail(email) 65 | try: 66 | self.sg.client.mail.send.post(request_body=mail) 67 | count += 1 68 | except HTTPError as e: 69 | if not self.fail_silently: 70 | raise 71 | return count 72 | 73 | def _build_sg_mail(self, email): 74 | mail = Mail() 75 | 76 | mail.set_from(self._process_email_addr(email.from_email)) 77 | mail.set_subject(email.subject) 78 | 79 | personalization = Personalization() 80 | for e in email.to: 81 | personalization.add_to(self._process_email_addr(e)) 82 | for e in email.cc: 83 | personalization.add_cc(self._process_email_addr(e)) 84 | for e in email.bcc: 85 | personalization.add_bcc(self._process_email_addr(e)) 86 | personalization.set_subject(email.subject) 87 | mail.add_content(Content("text/plain", email.body)) 88 | if isinstance(email, EmailMultiAlternatives): 89 | for alt in email.alternatives: 90 | if alt[1] == "text/html": 91 | mail.add_content(Content(alt[1], alt[0])) 92 | elif email.content_subtype == "html": 93 | mail.contents = [] 94 | mail.add_content(Content("text/plain", ' ')) 95 | mail.add_content(Content("text/html", email.body)) 96 | 97 | if hasattr(email, 'categories'): 98 | for c in email.categories: 99 | mail.add_category(Category(c)) 100 | 101 | if hasattr(email, 'custom_args'): 102 | for k, v in email.custom_args.items(): 103 | mail.add_custom_arg(CustomArg(k, v)) 104 | 105 | if hasattr(email, 'template_id'): 106 | mail.set_template_id(email.template_id) 107 | if hasattr(email, 'substitutions'): 108 | for key, value in email.substitutions.items(): 109 | personalization.add_substitution(Substitution(key, value)) 110 | 111 | # SendGrid does not support adding Reply-To as an extra 112 | # header, so it needs to be manually removed if it exists. 113 | reply_to_string = "" 114 | for key, value in email.extra_headers.items(): 115 | if key.lower() == "reply-to": 116 | reply_to_string = value 117 | else: 118 | mail.add_header({key: value}) 119 | # Note that if you set a "Reply-To" header *and* the reply_to 120 | # attribute, the header's value will be used. 121 | if not mail.reply_to and hasattr(email, "reply_to") and email.reply_to: 122 | # SendGrid only supports setting Reply-To to a single address. 123 | # See https://github.com/sendgrid/sendgrid-csharp/issues/339. 124 | reply_to_string = email.reply_to[0] 125 | # Determine whether reply_to contains a name and email address, or 126 | # just an email address. 127 | if reply_to_string: 128 | reply_to_name, reply_to_email = rfc822.parseaddr(reply_to_string) 129 | if reply_to_name and reply_to_email: 130 | mail.set_reply_to(Email(reply_to_email, reply_to_name)) 131 | elif reply_to_email: 132 | mail.set_reply_to(Email(reply_to_email)) 133 | 134 | for attachment in email.attachments: 135 | if isinstance(attachment, MIMEBase): 136 | attach = Attachment() 137 | attach.set_filename(attachment.get_filename()) 138 | attach.set_content(base64.b64encode(attachment.get_payload())) 139 | mail.add_attachment(attach) 140 | elif isinstance(attachment, tuple): 141 | attach = Attachment() 142 | attach.set_filename(attachment[0]) 143 | base64_attachment = base64.b64encode(attachment[1]) 144 | if sys.version_info >= (3,): 145 | attach.set_content(str(base64_attachment, 'utf-8')) 146 | else: 147 | attach.set_content(base64_attachment) 148 | attach.set_type(attachment[2]) 149 | mail.add_attachment(attach) 150 | 151 | mail.add_personalization(personalization) 152 | return mail.get() 153 | 154 | def _process_email_addr(self, email_addr): 155 | from_name, from_email = rfc822.parseaddr(email_addr) 156 | 157 | # Python sendgrid client should improve 158 | # sendgrid/helpers/mail/mail.py:164 159 | if not from_name: 160 | from_name = None 161 | 162 | return Email(from_email, from_name) 163 | -------------------------------------------------------------------------------- /sgbackend/version.py: -------------------------------------------------------------------------------- 1 | version_info = (4, 2, 0) # pragma: no cover 2 | __version__ = '.'.join(str(v) for v in version_info) # pragma: no cover 3 | -------------------------------------------------------------------------------- /tests/test_api_key.py: -------------------------------------------------------------------------------- 1 | from sgbackend import SendGridBackend 2 | 3 | 4 | def test_read_from_settings_by_default(settings): 5 | """API key should be read from settings if not provided in init.""" 6 | settings.SENDGRID_API_KEY = 'somerandom key' 7 | sg = SendGridBackend() 8 | 9 | assert sg.api_key == settings.SENDGRID_API_KEY 10 | 11 | 12 | def test_read_from_init_if_provided(settings): 13 | """Should pick up API key from init, if provided.""" 14 | settings.SENDGRID_API_KEY = 'somerandom key' 15 | actual_key = 'another' 16 | 17 | sg = SendGridBackend(api_key=actual_key) 18 | 19 | assert sg.api_key == actual_key 20 | 21 | """ Note: no API key configuration is tested in test_mail.""" -------------------------------------------------------------------------------- /tests/test_mail.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.core.mail import EmailMessage 4 | from django.core.mail import EmailMultiAlternatives 5 | from django.test import SimpleTestCase as TestCase 6 | 7 | from sgbackend import SendGridBackend 8 | 9 | settings.configure() 10 | 11 | 12 | class SendGridBackendTests(TestCase): 13 | def test_raises_if_sendgrid_api_key_doesnt_exists(self): 14 | with self.assertRaises(ImproperlyConfigured): 15 | SendGridBackend() 16 | 17 | def test_build_empty_sg_mail(self): 18 | msg = EmailMessage() 19 | with self.settings(SENDGRID_API_KEY='test_key'): 20 | mail = SendGridBackend()._build_sg_mail(msg) 21 | self.assertEqual( 22 | mail, 23 | {'from': {'email': 'webmaster@localhost'}, 24 | 'subject': '', 25 | 'content': [{'type': 'text/plain', 'value': ''}], 26 | 'personalizations': [{'subject': ''}]} 27 | ) 28 | 29 | def test_build_w_to_sg_email(self): 30 | msg = EmailMessage(to=('andrii.soldatenko@test.com',)) 31 | with self.settings(SENDGRID_API_KEY='test_key'): 32 | mail = SendGridBackend()._build_sg_mail(msg) 33 | self.assertEqual( 34 | mail, 35 | {'content': [{'value': '', 'type': 'text/plain'}], 36 | 'personalizations': [ 37 | {'to': [{'email': 'andrii.soldatenko@test.com'}], 38 | 'subject': ''}], 39 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 40 | ) 41 | 42 | # Test using "name " format. 43 | msg = EmailMessage(to=('Andrii Soldatenko ',)) 44 | with self.settings(SENDGRID_API_KEY='test_key'): 45 | mail = SendGridBackend()._build_sg_mail(msg) 46 | self.assertEqual( 47 | mail, 48 | {'content': [{'value': '', 'type': 'text/plain'}], 49 | 'personalizations': [ 50 | {'to': [ 51 | {'name': 'Andrii Soldatenko', 52 | 'email': 'andrii.soldatenko@test.com'}], 53 | 'subject': ''}], 54 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 55 | ) 56 | 57 | def test_build_w_cc_sg_email(self): 58 | msg = EmailMessage(cc=('andrii.soldatenko@test.com',)) 59 | with self.settings(SENDGRID_API_KEY='test_key'): 60 | mail = SendGridBackend()._build_sg_mail(msg) 61 | self.assertEqual( 62 | mail, 63 | {'content': [{'value': '', 'type': 'text/plain'}], 64 | 'personalizations': [ 65 | {'cc': [{'email': 'andrii.soldatenko@test.com'}], 66 | 'subject': ''}], 67 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 68 | ) 69 | 70 | # Test using "name " format. 71 | msg = EmailMessage(cc=('Andrii Soldatenko ',)) 72 | with self.settings(SENDGRID_API_KEY='test_key'): 73 | mail = SendGridBackend()._build_sg_mail(msg) 74 | self.assertEqual( 75 | mail, 76 | {'content': [{'value': '', 'type': 'text/plain'}], 77 | 'personalizations': [ 78 | {'cc': [ 79 | {'name': 'Andrii Soldatenko', 80 | 'email': 'andrii.soldatenko@test.com'}], 81 | 'subject': ''}], 82 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 83 | ) 84 | 85 | def test_build_w_bcc_sg_email(self): 86 | msg = EmailMessage(bcc=('andrii.soldatenko@test.com',)) 87 | with self.settings(SENDGRID_API_KEY='test_key'): 88 | mail = SendGridBackend()._build_sg_mail(msg) 89 | self.assertEqual( 90 | mail, 91 | {'content': [{'value': '', 'type': 'text/plain'}], 92 | 'personalizations': [ 93 | {'bcc': [{'email': 'andrii.soldatenko@test.com'}], 94 | 'subject': ''}], 95 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 96 | ) 97 | 98 | # Test using "name " format. 99 | msg = EmailMessage(bcc=('Andrii Soldatenko ',)) 100 | with self.settings(SENDGRID_API_KEY='test_key'): 101 | mail = SendGridBackend()._build_sg_mail(msg) 102 | self.assertEqual( 103 | mail, 104 | {'content': [{'value': '', 'type': 'text/plain'}], 105 | 'personalizations': [ 106 | {'bcc': [ 107 | {'name': 'Andrii Soldatenko', 108 | 'email': 'andrii.soldatenko@test.com'}], 109 | 'subject': ''}], 110 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 111 | ) 112 | 113 | def test_build_w_reply_to_sg_email(self): 114 | # Test setting a Reply-To header. 115 | msg = EmailMessage() 116 | msg.extra_headers = {'Reply-To': 'andrii.soldatenko@test.com'} 117 | with self.settings(SENDGRID_API_KEY='test_key'): 118 | mail = SendGridBackend()._build_sg_mail(msg) 119 | self.assertEqual( 120 | mail, 121 | {'content': [{'value': '', 'type': 'text/plain'}], 122 | 'personalizations': [{'subject': ''}], 123 | 'reply_to': {'email': 'andrii.soldatenko@test.com'}, 124 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 125 | ) 126 | # Test using the reply_to attribute. 127 | msg = EmailMessage(reply_to=('andrii.soldatenko@test.com',)) 128 | with self.settings(SENDGRID_API_KEY='test_key'): 129 | mail = SendGridBackend()._build_sg_mail(msg) 130 | self.assertEqual( 131 | mail, 132 | {'content': [{'value': '', 'type': 'text/plain'}], 133 | 'personalizations': [{'subject': ''}], 134 | 'reply_to': {'email': 'andrii.soldatenko@test.com'}, 135 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 136 | ) 137 | # Test using "name " format. 138 | msg = EmailMessage( 139 | reply_to=('Andrii Soldatenko ',)) 140 | with self.settings(SENDGRID_API_KEY='test_key'): 141 | mail = SendGridBackend()._build_sg_mail(msg) 142 | self.assertEqual( 143 | mail, 144 | {'content': [{'value': '', 'type': 'text/plain'}], 145 | 'personalizations': [{'subject': ''}], 146 | 'reply_to': { 147 | 'name': 'Andrii Soldatenko', 148 | 'email': 'andrii.soldatenko@test.com'}, 149 | 'from': {'email': 'webmaster@localhost'}, 'subject': ''} 150 | ) 151 | 152 | def test_build_empty_multi_alternatives_sg_email(self): 153 | html_content = '

This is an important message.

' 154 | msg = EmailMultiAlternatives() 155 | msg.attach_alternative(html_content, "text/html") 156 | with self.settings(SENDGRID_API_KEY='test_key'): 157 | mail = SendGridBackend()._build_sg_mail(msg) 158 | self.assertEqual( 159 | mail, 160 | {'content': [{'type': 'text/plain', 'value': ''}, 161 | {'type': 'text/html', 162 | 'value': '

This is an ' 163 | 'important ' 164 | 'message.

'}], 165 | 'from': {'email': 'webmaster@localhost'}, 166 | 'personalizations': [{'subject': ''}], 167 | 'subject': ''} 168 | ) 169 | 170 | def test_build_sg_email_w_categories(self): 171 | msg = EmailMessage() 172 | msg.categories = ['name'] 173 | with self.settings(SENDGRID_API_KEY='test_key'): 174 | mail = SendGridBackend()._build_sg_mail(msg) 175 | self.assertEqual( 176 | mail, 177 | {'categories': ['name'], 178 | 'content': [{'type': 'text/plain', 'value': ''}], 179 | 'from': {'email': 'webmaster@localhost'}, 180 | 'personalizations': [{'subject': ''}], 181 | 'subject': '' 182 | } 183 | ) 184 | 185 | def test_build_sg_email_w_template_id(self): 186 | msg = EmailMessage() 187 | msg.template_id = 'template_id_123456' 188 | with self.settings(SENDGRID_API_KEY='test_key'): 189 | mail = SendGridBackend()._build_sg_mail(msg) 190 | self.assertEqual( 191 | mail, 192 | {'template_id': 'template_id_123456', 193 | 'content': [{'type': 'text/plain', 'value': ''}], 194 | 'from': {'email': 'webmaster@localhost'}, 195 | 'personalizations': [{'subject': ''}], 196 | 'subject': '' 197 | } 198 | ) 199 | 200 | def test_build_sg_email_w_substitutions(self): 201 | msg = EmailMessage() 202 | msg.substitutions = {} 203 | with self.settings(SENDGRID_API_KEY='test_key'): 204 | mail = SendGridBackend()._build_sg_mail(msg) 205 | self.assertEqual( 206 | mail, 207 | {'content': [{'type': 'text/plain', 'value': ''}], 208 | 'from': {'email': 'webmaster@localhost'}, 209 | 'personalizations': [{'subject': ''}], 210 | 'subject': ''} 211 | ) 212 | 213 | def test_build_sg_email_w_extra_headers(self): 214 | msg = EmailMessage() 215 | msg.extra_headers = {'EXTRA_HEADER': 'VALUE'} 216 | with self.settings(SENDGRID_API_KEY='test_key'): 217 | mail = SendGridBackend()._build_sg_mail(msg) 218 | self.assertEqual( 219 | mail, 220 | {'content': [{'type': 'text/plain', 'value': ''}], 221 | 'from': {'email': 'webmaster@localhost'}, 222 | 'headers': {'EXTRA_HEADER': 'VALUE'}, 223 | 'personalizations': [{'subject': ''}], 224 | 'subject': ''} 225 | ) 226 | 227 | def test_build_sg_email_w_custom_args(self): 228 | msg = EmailMessage() 229 | msg.custom_args = {'custom_arg1': '12345-abcdef'} 230 | 231 | with self.settings(SENDGRID_API_KEY='test_key'): 232 | mail = SendGridBackend()._build_sg_mail(msg) 233 | 234 | self.assertEqual( 235 | mail, 236 | {'content': [{'type': 'text/plain', 'value': ''}], 237 | 'custom_args': {'custom_arg1': '12345-abcdef'}, 238 | 'from': {'email': 'webmaster@localhost'}, 239 | 'personalizations': [{'subject': ''}], 240 | 'subject': ''} 241 | ) 242 | -------------------------------------------------------------------------------- /tests/test_send_messages.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import Mock 3 | except ImportError: 4 | from mock import Mock 5 | try: 6 | from urllib.error import HTTPError 7 | except ImportError: 8 | from urllib2 import HTTPError 9 | 10 | from django.core.mail import EmailMessage 11 | from django.test import SimpleTestCase 12 | 13 | from sgbackend import SendGridBackend 14 | 15 | 16 | class SendMessagesTestCase(SimpleTestCase): 17 | """Tests for SendGridBackend.test_messages().""" 18 | 19 | def setUp(self): 20 | self.test_message = EmailMessage( 21 | subject="Test email message", 22 | body="Lorem ipsum!", 23 | from_email="example@example.com", 24 | to=["example2@example.com"], 25 | ) 26 | 27 | def test_sending_without_emails(self): 28 | """ 29 | Verify that send_messages returns nothing if no messages are passed. 30 | """ 31 | sendgrid_backend = SendGridBackend(api_key="test") 32 | self.assertEqual(sendgrid_backend.send_messages(emails=[]), None) 33 | 34 | def test_sending(self): 35 | """Verify that send_messages returns sent count if message is sent.""" 36 | sendgrid_backend = SendGridBackend(api_key="test") 37 | sendgrid_backend.sg.client = Mock() 38 | self.assertEqual( 39 | sendgrid_backend.send_messages(emails=[self.test_message]), 1 40 | ) 41 | 42 | def test_failing_silently(self): 43 | """Verify that send_messages can fail silently.""" 44 | sendgrid_backend = SendGridBackend(api_key="test") 45 | sendgrid_backend.sg.client = Mock() 46 | http_error = HTTPError(url="", code=999, msg=None, hdrs=None, fp=None) 47 | sendgrid_backend.sg.client.mail.send.post = Mock( 48 | side_effect=http_error 49 | ) 50 | 51 | self.assertFalse(sendgrid_backend.fail_silently) 52 | with self.assertRaises(HTTPError): 53 | sendgrid_backend.send_messages(emails=[self.test_message]) 54 | 55 | sendgrid_backend.fail_silently = True 56 | self.assertEqual( 57 | sendgrid_backend.send_messages(emails=[self.test_message]), 0 58 | ) 59 | --------------------------------------------------------------------------------