├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── getting_started.rst │ ├── index.rst │ ├── sample_usage.rst │ ├── settings.rst │ └── templates_syntax.rst ├── examples ├── __init__.py ├── config │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── simple │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── urls.py │ └── views.py └── templates │ ├── html_mail.html │ ├── text_and_html_mail.html │ └── text_mail.html ├── requirements ├── base └── test ├── setup.py ├── templated_mail ├── __init__.py └── mail.py ├── tests ├── __init__.py ├── helpers.py ├── settings.py ├── templates │ ├── extends.html │ ├── html_mail.html │ ├── nested_extends.html │ ├── text_and_html_mail.html │ └── text_mail.html └── test_mail.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # database files 2 | *.sqlite3 3 | 4 | # pycharm files 5 | .idea/ 6 | 7 | # Distribution 8 | build/ 9 | dist/ 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # Translations 17 | *.mo 18 | *.pot 19 | 20 | # Test files 21 | .cache/ 22 | .tox/ 23 | .coverage 24 | htmlcov/ 25 | 26 | # dotenv 27 | .env 28 | 29 | # virtualenv 30 | env/ 31 | .venv/ 32 | venv/ 33 | ENV/ 34 | 35 | # osx 36 | *.DS_Store 37 | 38 | # logs 39 | *.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | python: 5 | - "2.7" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | - "3.7-dev" 10 | 11 | env: 12 | - DJANGO=1.11 13 | - DJANGO=2.0 14 | - DJANGO=2.1 15 | - DJANGO=master 16 | 17 | matrix: 18 | fast_finish: true 19 | exclude: 20 | - python: "2.7" 21 | env: DJANGO=2.0 22 | - python: "2.7" 23 | env: DJANGO=2.1 24 | - python: "2.7" 25 | env: DJANGO=master 26 | - python: "3.4" 27 | env: DJANGO=2.1 28 | - python: "3.4" 29 | env: DJANGO=master 30 | allow_failures: 31 | - env: DJANGO=master 32 | - python: "3.7-dev" 33 | 34 | install: 35 | - travis_retry pip install -U tox-travis 36 | 37 | script: 38 | - tox 39 | 40 | after_success: 41 | - travis_retry pip install -U codecov 42 | - codecov 43 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Change Log 3 | ========== 4 | 5 | This document records all notable changes to django-templated-mail. 6 | This project adheres to `Semantic Versioning `_. 7 | 8 | --------------------- 9 | `1.1.1`_ (2017-02-01) 10 | --------------------- 11 | 12 | * Bugfix: ``from_email`` does not fallback to ``DEFAULT_FROM_EMAIL`` 13 | 14 | 15 | --------------------- 16 | `1.1.0`_ (2017-01-29) 17 | --------------------- 18 | 19 | * Add support for ``reply_to`` parameter in ``BaseEmailMessage.send`` method 20 | * Add support for ``from_email`` parameter in ``BaseEmailMessage.send`` method 21 | * Add support for Django 2.0 22 | * Remove support for Django 1.10 23 | * Fix passing context to email classes with context provided via mixin 24 | * Fix invalid release years in release notes 25 | 26 | --------------------- 27 | `1.0.0`_ (2017-10-06) 28 | --------------------- 29 | 30 | * Breaking API: Update ``set_context_data`` to ``get_context_data`` 31 | * Add basic documentation 32 | * Add basic examples 33 | * Update templates rendering to happen on send 34 | * Update dependencies 35 | * Remove Python 3.3 from supported versions 36 | 37 | --------------------- 38 | `0.2.0`_ (2017-09-22) 39 | --------------------- 40 | 41 | * Add support for CC and BCC 42 | * Update name of ``BaseEmailMessage.send_to`` to ``BaseEmailMessage.send`` 43 | 44 | --------------------- 45 | `0.1.1`_ (2017-09-15) 46 | --------------------- 47 | 48 | * Bugfix: Issue with template nodes requiring template to be bound to context 49 | * Bugfix: Issue with whitespaces around content blocks 50 | 51 | --------------------- 52 | `0.1.0`_ (2017-09-15) 53 | --------------------- 54 | 55 | * Initial release of the project. Its goal is to provide simple API for sending 56 | emails using Django template system. For more information and to get started see 57 | `README `_. 58 | 59 | 60 | .. _0.1.0: https://github.com/sunscrapers/django-templated-mail/compare/3bc71b3...0.1.0 61 | .. _0.1.1: https://github.com/sunscrapers/django-templated-mail/compare/0.1.0...0.1.1 62 | .. _0.2.0: https://github.com/sunscrapers/django-templated-mail/compare/0.1.1...0.2.0 63 | .. _1.0.0: https://github.com/sunscrapers/django-templated-mail/compare/0.2.0...1.0.0 64 | .. _1.1.0: https://github.com/sunscrapers/django-templated-mail/compare/1.0.0...1.1.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 SUNSCRAPERS 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pipenv install --dev 3 | pipenv run pip install -e . 4 | 5 | test: 6 | pipenv run py.test --ds=tests.settings --capture=no --cov-report term-missing --cov-report html --cov=templated_mail tests 7 | pipenv run flake8 . 8 | 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | 5 | [packages] 6 | django = "*" 7 | 8 | 9 | 10 | [dev-packages] 11 | 12 | ipython = "*" 13 | ipdb = "*" 14 | pytest = "*" 15 | "pytest-cov" = "*" 16 | "pytest-django" = "*" 17 | "flake8" = "*" 18 | "pytest-pythonpath" = "*" 19 | twine = "*" 20 | wheel = "*" 21 | sphinx = "*" 22 | "sphinx-rtd-theme" = "*" -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "8c45cd4fef47bf79225773acb1db2d4aadc400a1db629f7884a7514499de4cf6" 5 | }, 6 | "host-environment-markers": { 7 | "implementation_name": "cpython", 8 | "implementation_version": "3.6.2", 9 | "os_name": "posix", 10 | "platform_machine": "x86_64", 11 | "platform_python_implementation": "CPython", 12 | "platform_release": "4.13.3-1-ARCH", 13 | "platform_system": "Linux", 14 | "platform_version": "#1 SMP PREEMPT Thu Sep 21 20:33:16 CEST 2017", 15 | "python_full_version": "3.6.2", 16 | "python_version": "3.6", 17 | "sys_platform": "linux" 18 | }, 19 | "pipfile-spec": 6, 20 | "requires": {}, 21 | "sources": [ 22 | { 23 | "url": "https://pypi.python.org/simple", 24 | "verify_ssl": true 25 | } 26 | ] 27 | }, 28 | "default": { 29 | "django": { 30 | "hashes": [ 31 | "sha256:7ab6a9c798a5f9f359ee6da3677211f883fb02ef32cebe9b29751eb7a871febf", 32 | "sha256:c3b42ca1efa1c0a129a9e863134cc3fe705c651dea3a04a7998019e522af0c60" 33 | ], 34 | "version": "==1.11.6" 35 | }, 36 | "pytz": { 37 | "hashes": [ 38 | "sha256:c883c2d6670042c7bc1688645cac73dd2b03193d1f7a6847b6154e96890be06d", 39 | "sha256:03c9962afe00e503e2d96abab4e8998a0f84d4230fa57afe1e0528473698cdd9", 40 | "sha256:487e7d50710661116325747a9cd1744d3323f8e49748e287bc9e659060ec6bf9", 41 | "sha256:43f52d4c6a0be301d53ebd867de05e2926c35728b3260157d274635a0a947f1c", 42 | "sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67", 43 | "sha256:54a935085f7bf101f86b2aff75bd9672b435f51c3339db2ff616e66845f2b8f9", 44 | "sha256:39504670abb5dae77f56f8eb63823937ce727d7cdd0088e6909e6dcac0f89043", 45 | "sha256:ddc93b6d41cfb81266a27d23a79e13805d4a5521032b512643af8729041a81b4", 46 | "sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589" 47 | ], 48 | "version": "==2017.2" 49 | } 50 | }, 51 | "develop": { 52 | "alabaster": { 53 | "hashes": [ 54 | "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732", 55 | "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0" 56 | ], 57 | "version": "==0.7.10" 58 | }, 59 | "babel": { 60 | "hashes": [ 61 | "sha256:f20b2acd44f587988ff185d8949c3e208b4b3d5d20fcab7d91fe481ffa435528", 62 | "sha256:6007daf714d0cd5524bbe436e2d42b3c20e68da66289559341e48d2cd6d25811" 63 | ], 64 | "version": "==2.5.1" 65 | }, 66 | "certifi": { 67 | "hashes": [ 68 | "sha256:54a07c09c586b0e4c619f02a5e94e36619da8e2b053e20f594348c0611803704", 69 | "sha256:40523d2efb60523e113b44602298f0960e900388cf3bb6043f645cf57ea9e3f5" 70 | ], 71 | "version": "==2017.7.27.1" 72 | }, 73 | "chardet": { 74 | "hashes": [ 75 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", 76 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae" 77 | ], 78 | "version": "==3.0.4" 79 | }, 80 | "coverage": { 81 | "hashes": [ 82 | "sha256:c1456f66c536010cf9e4633a8853a9153e8fd588393695295afd4d0fc16c1d74", 83 | "sha256:97a7ec51cdde3a386e390b159b20f247ccb478084d925c75f1faa3d26c01335e", 84 | "sha256:83e955b975666b5a07d217135e7797857ce844eb340a99e46cc25525120417c4", 85 | "sha256:483ed14080c5301048128bb027b77978c632dd9e92e3ecb09b7e28f5b92abfcf", 86 | "sha256:ef574ab9640bcfa2f3c671831faf03f65788945fdf8efa4d4a1fffc034838e2a", 87 | "sha256:c5a205b4da3c624f5119dc4d84240789b5906bb8468902ec22dcc4aad8aa4638", 88 | "sha256:5dea90ed140e7fa9bc00463313f9bc4a6e6aff297b4969615e7a688615c4c4d2", 89 | "sha256:f9e83b39d29c2815a38e4118d776b482d4082b5bf9c9147fbc99a3f83abe480a", 90 | "sha256:700040c354f0230287906b1276635552a3def4b646e0145555bc9e2e5da9e365", 91 | "sha256:7f1eacae700c66c3d7362a433b228599c9d94a5a3a52613dddd9474e04deb6bc", 92 | "sha256:13ef9f799c8fb45c446a239df68034de3a6f3de274881b088bebd7f5661f79f8", 93 | "sha256:dfb011587e2b7299112f08a2a60d2601706aac9abde37aa1177ea825adaed923", 94 | "sha256:381be5d31d3f0d912334cf2c159bc7bea6bfe6b0e3df6061a3bf2bf88359b1f6", 95 | "sha256:83a477ac4f55a6ef59552683a0544d47b68a85ce6a80fd0ca6b3dc767f6495fb", 96 | "sha256:dfd35f1979da31bcabbe27bcf78d4284d69870731874af629082590023a77336", 97 | "sha256:9681efc2d310cfc53863cc6f63e88ebe7a48124550fa822147996cb09390b6ab", 98 | "sha256:53770b20ac5b4a12e99229d4bae57af0945be87cc257fce6c6c7571a39f0c5d4", 99 | "sha256:8801880d32f11b6df11c32a961e186774b4634ae39d7c43235f5a24368a85f07", 100 | "sha256:16db2c69a1acbcb3c13211e9f954e22b22a729909d81f983b6b9badacc466eda", 101 | "sha256:ef43a06a960b46c73c018704051e023ee6082030f145841ffafc8728039d5a88", 102 | "sha256:c3e2736664a6074fc9bd54fb643f5af0fc60bfedb2963b3d3f98c7450335e34c", 103 | "sha256:17709e22e4c9f5412ba90f446fb13b245cc20bf4a60377021bbff6c0f1f63e7c", 104 | "sha256:a2f7106d1167825c4115794c2ba57cc3b15feb6183db5328fa66f94c12902d8b", 105 | "sha256:2a08e978f402696c6956eee9d1b7e95d3ad042959b71bafe1f3e4557cbd6e0ac", 106 | "sha256:57f510bb16efaec0b6f371b64a8000c62e7e3b3e48e8b0a5745ade078d849814", 107 | "sha256:0f1883eab9c19aa243f51308751b8a2a547b9b817b721cc0ecf3efb99fafbea7", 108 | "sha256:e00fe141e22ce6e9395aa24d862039eb180c6b7e89df0bbaf9765e9aebe560a9", 109 | "sha256:ec596e4401553caa6dd2e3349ce47f9ef82c1f1bcba5d8ac3342724f0df8d6ff", 110 | "sha256:c820a533a943ebc860acc0ce6a00dd36e0fdf2c6f619ff8225755169428c5fa2", 111 | "sha256:b7f7283eb7badd2b8a9c6a9d6eeca200a0a24db6be79baee2c11398f978edcaa", 112 | "sha256:a5ed27ad3e8420b2d6b625dcbd3e59488c14ccc06030167bcf14ffb0f4189b77", 113 | "sha256:d7b70b7b4eb14d0753d33253fe4f121ca99102612e2719f0993607deb30c6f33", 114 | "sha256:4047dc83773869701bde934fb3c4792648eda7c0e008a77a0aec64157d246801", 115 | "sha256:7a9c44400ee0f3b4546066e0710e1250fd75831adc02ab99dda176ad8726f424", 116 | "sha256:0f649e68db74b1b5b8ca4161d08eb2b8fa8ae11af1ebfb80e80e112eb0ef5300", 117 | "sha256:52964fae0fafef8bd283ad8e9a9665205a9fdf912535434defc0ec3def1da26b", 118 | "sha256:36aa6c8db83bc27346ddcd8c2a60846a7178ecd702672689d3ea1828eb1a4d11", 119 | "sha256:9824e15b387d331c0fc0fef905a539ab69784368a1d6ac3db864b4182e520948", 120 | "sha256:4a678e1b9619a29c51301af61ab84122e2f8cc7a0a6b40854b808ac6be604300", 121 | "sha256:8bb7c8dca54109b61013bc4114d96effbf10dea136722c586bce3a5d9fc4e730", 122 | "sha256:1a41d621aa9b6ab6457b557a754d50aaff0813fad3453434de075496fca8a183", 123 | "sha256:0fa423599fc3d9e18177f913552cdb34a8d9ad33efcf52a98c9d4b644edb42c5", 124 | "sha256:e61a4ba0b2686040cb4828297c7e37bcaf3a1a1c0bc0dbe46cc789dde51a80fa", 125 | "sha256:ce9ef0fc99d11d418662e36fd8de6d71b19ec87c2eab961a117cc9d087576e72" 126 | ], 127 | "version": "==4.4.1" 128 | }, 129 | "decorator": { 130 | "hashes": [ 131 | "sha256:95a26b17806e284452bfd97fa20aa1e8cb4ee23542bda4dbac5e4562aa1642cd", 132 | "sha256:7cb64d38cb8002971710c8899fbdfb859a23a364b7c99dab19d1f719c2ba16b5" 133 | ], 134 | "version": "==4.1.2" 135 | }, 136 | "docutils": { 137 | "hashes": [ 138 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6", 139 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 140 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274" 141 | ], 142 | "version": "==0.14" 143 | }, 144 | "flake8": { 145 | "hashes": [ 146 | "sha256:f1a9d8886a9cbefb52485f4f4c770832c7fb569c084a9a314fb1eaa37c0c2c86", 147 | "sha256:c20044779ff848f67f89c56a0e4624c04298cd476e25253ac0c36f910a1a11d8" 148 | ], 149 | "version": "==3.4.1" 150 | }, 151 | "idna": { 152 | "hashes": [ 153 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4", 154 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f" 155 | ], 156 | "version": "==2.6" 157 | }, 158 | "imagesize": { 159 | "hashes": [ 160 | "sha256:6ebdc9e0ad188f9d1b2cdd9bc59cbe42bf931875e829e7a595e6b3abdc05cdfb", 161 | "sha256:0ab2c62b87987e3252f89d30b7cedbec12a01af9274af9ffa48108f2c13c6062" 162 | ], 163 | "version": "==0.7.1" 164 | }, 165 | "ipdb": { 166 | "hashes": [ 167 | "sha256:9ea256b4280fbe12840fb9dfc3ce498c6c6de03352eca293e4400b0dfbed2b28" 168 | ], 169 | "version": "==0.10.3" 170 | }, 171 | "ipython": { 172 | "hashes": [ 173 | "sha256:fcc6d46f08c3c4de7b15ae1c426e15be1b7932bcda9d83ce1a4304e8c1129df3", 174 | "sha256:51c158a6c8b899898d1c91c6b51a34110196815cc905f9be0fa5878e19355608" 175 | ], 176 | "version": "==6.2.1" 177 | }, 178 | "ipython-genutils": { 179 | "hashes": [ 180 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 181 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 182 | ], 183 | "version": "==0.2.0" 184 | }, 185 | "jedi": { 186 | "hashes": [ 187 | "sha256:3af518490ffcd00a3074c135b42511e081575e9abd115c216a34491411ceebb0", 188 | "sha256:f6d5973573e76b1fd2ea75f6dcd6445d02d41ff3af5fc61b275b4e323d1dd396" 189 | ], 190 | "version": "==0.11.0" 191 | }, 192 | "jinja2": { 193 | "hashes": [ 194 | "sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054", 195 | "sha256:ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff" 196 | ], 197 | "version": "==2.9.6" 198 | }, 199 | "markupsafe": { 200 | "hashes": [ 201 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" 202 | ], 203 | "version": "==1.0" 204 | }, 205 | "mccabe": { 206 | "hashes": [ 207 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 208 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 209 | ], 210 | "version": "==0.6.1" 211 | }, 212 | "parso": { 213 | "hashes": [ 214 | "sha256:b573acb69f66a970197b5fdbbdfad3b8a417a520e383133b2b4e708f104bfc9a", 215 | "sha256:c5279916bb417aa2bf634648ff895cf35dce371d7319744884827bfad06f8d7b" 216 | ], 217 | "version": "==0.1.0" 218 | }, 219 | "pexpect": { 220 | "hashes": [ 221 | "sha256:f853b52afaf3b064d29854771e2db509ef80392509bde2dd7a6ecf2dfc3f0018", 222 | "sha256:3d132465a75b57aa818341c6521392a06cc660feb3988d7f1074f39bd23c9a92" 223 | ], 224 | "markers": "sys_platform != 'win32'", 225 | "version": "==4.2.1" 226 | }, 227 | "pickleshare": { 228 | "hashes": [ 229 | "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5", 230 | "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b" 231 | ], 232 | "version": "==0.7.4" 233 | }, 234 | "pkginfo": { 235 | "hashes": [ 236 | "sha256:31a49103180ae1518b65d3f4ce09c784e2bc54e338197668b4fb7dc539521024", 237 | "sha256:bb1a6aeabfc898f5df124e7e00303a5b3ec9a489535f346bfbddb081af93f89e" 238 | ], 239 | "version": "==1.4.1" 240 | }, 241 | "prompt-toolkit": { 242 | "hashes": [ 243 | "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", 244 | "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", 245 | "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" 246 | ], 247 | "version": "==1.0.15" 248 | }, 249 | "ptyprocess": { 250 | "hashes": [ 251 | "sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a", 252 | "sha256:e64193f0047ad603b71f202332ab5527c5e52aa7c8b609704fc28c0dc20c4365" 253 | ], 254 | "version": "==0.5.2" 255 | }, 256 | "py": { 257 | "hashes": [ 258 | "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a", 259 | "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3" 260 | ], 261 | "version": "==1.4.34" 262 | }, 263 | "pycodestyle": { 264 | "hashes": [ 265 | "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", 266 | "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766" 267 | ], 268 | "version": "==2.3.1" 269 | }, 270 | "pyflakes": { 271 | "hashes": [ 272 | "sha256:cc5eadfb38041f8366128786b4ca12700ed05bbf1403d808e89d57d67a3875a7", 273 | "sha256:aa0d4dff45c0cc2214ba158d29280f8fa1129f3e87858ef825930845146337f4" 274 | ], 275 | "version": "==1.5.0" 276 | }, 277 | "pygments": { 278 | "hashes": [ 279 | "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", 280 | "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" 281 | ], 282 | "version": "==2.2.0" 283 | }, 284 | "pytest": { 285 | "hashes": [ 286 | "sha256:81a25f36a97da3313e1125fce9e7bbbba565bc7fec3c5beb14c262ddab238ac1", 287 | "sha256:27fa6617efc2869d3e969a3e75ec060375bfb28831ade8b5cdd68da3a741dc3c" 288 | ], 289 | "version": "==3.2.3" 290 | }, 291 | "pytest-cov": { 292 | "hashes": [ 293 | "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec", 294 | "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d" 295 | ], 296 | "version": "==2.5.1" 297 | }, 298 | "pytest-django": { 299 | "hashes": [ 300 | "sha256:00995c2999b884a38ae9cd30a8c00ed32b3d38c1041250ea84caf18085589662", 301 | "sha256:038ccc5a9daa1b1b0eb739ab7dce54e495811eca5ea3af4815a2a3ac45152309" 302 | ], 303 | "version": "==3.1.2" 304 | }, 305 | "pytest-pythonpath": { 306 | "hashes": [ 307 | "sha256:2d506b8d7dbc2535a16c888211b7319ad32b3e73444bd9dbb1dd19427a6c7414" 308 | ], 309 | "version": "==0.7.1" 310 | }, 311 | "pytz": { 312 | "hashes": [ 313 | "sha256:c883c2d6670042c7bc1688645cac73dd2b03193d1f7a6847b6154e96890be06d", 314 | "sha256:03c9962afe00e503e2d96abab4e8998a0f84d4230fa57afe1e0528473698cdd9", 315 | "sha256:487e7d50710661116325747a9cd1744d3323f8e49748e287bc9e659060ec6bf9", 316 | "sha256:43f52d4c6a0be301d53ebd867de05e2926c35728b3260157d274635a0a947f1c", 317 | "sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67", 318 | "sha256:54a935085f7bf101f86b2aff75bd9672b435f51c3339db2ff616e66845f2b8f9", 319 | "sha256:39504670abb5dae77f56f8eb63823937ce727d7cdd0088e6909e6dcac0f89043", 320 | "sha256:ddc93b6d41cfb81266a27d23a79e13805d4a5521032b512643af8729041a81b4", 321 | "sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589" 322 | ], 323 | "version": "==2017.2" 324 | }, 325 | "requests": { 326 | "hashes": [ 327 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", 328 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" 329 | ], 330 | "version": "==2.18.4" 331 | }, 332 | "requests-toolbelt": { 333 | "hashes": [ 334 | "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", 335 | "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" 336 | ], 337 | "version": "==0.8.0" 338 | }, 339 | "simplegeneric": { 340 | "hashes": [ 341 | "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" 342 | ], 343 | "version": "==0.8.1" 344 | }, 345 | "six": { 346 | "hashes": [ 347 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", 348 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" 349 | ], 350 | "version": "==1.11.0" 351 | }, 352 | "snowballstemmer": { 353 | "hashes": [ 354 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89", 355 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128" 356 | ], 357 | "version": "==1.2.1" 358 | }, 359 | "sphinx": { 360 | "hashes": [ 361 | "sha256:3e70eb94f7e81b47e0545ebc26b758193b6c8b222e152ded99b9c972e971c731", 362 | "sha256:f101efd87fbffed8d8aca6ef307fec57693334f39d32efcbc2fc96ed129f4a3e" 363 | ], 364 | "version": "==1.6.4" 365 | }, 366 | "sphinx-rtd-theme": { 367 | "hashes": [ 368 | "sha256:62ee4752716e698bad7de8a18906f42d33664128eea06c46b718fc7fbd1a9f5c", 369 | "sha256:2df74b8ff6fae6965c527e97cca6c6c944886aae474b490e17f92adfbe843417" 370 | ], 371 | "version": "==0.2.4" 372 | }, 373 | "sphinxcontrib-websupport": { 374 | "hashes": [ 375 | "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2", 376 | "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9" 377 | ], 378 | "version": "==1.0.1" 379 | }, 380 | "tqdm": { 381 | "hashes": [ 382 | "sha256:8c281e644b62dee309259a6fe49e2d7eb3d6fefea978855394aeb3c099b13201", 383 | "sha256:5e6db2f1f73b97139da33268838966337d19baf539926ca3f7116620aacb7a00" 384 | ], 385 | "version": "==4.19.1.post1" 386 | }, 387 | "traitlets": { 388 | "hashes": [ 389 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9", 390 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835" 391 | ], 392 | "version": "==4.3.2" 393 | }, 394 | "twine": { 395 | "hashes": [ 396 | "sha256:d3ce5c480c22ccfb761cd358526e862b32546d2fe4bc93d46b5cf04ea3cc46ca", 397 | "sha256:caa45b7987fc96321258cd7668e3be2ff34064f5c66d2d975b641adca659c1ab" 398 | ], 399 | "version": "==1.9.1" 400 | }, 401 | "urllib3": { 402 | "hashes": [ 403 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", 404 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" 405 | ], 406 | "version": "==1.22" 407 | }, 408 | "wcwidth": { 409 | "hashes": [ 410 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c", 411 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e" 412 | ], 413 | "version": "==0.1.7" 414 | }, 415 | "wheel": { 416 | "hashes": [ 417 | "sha256:e721e53864f084f956f40f96124a74da0631ac13fbbd1ba99e8e2b5e9cafdf64", 418 | "sha256:9515fe0a94e823fd90b08d22de45d7bde57c90edce705b22f5e1ecf7e1b653c8" 419 | ], 420 | "version": "==0.30.0" 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | django-templated-mail 3 | ===================== 4 | 5 | .. image:: https://img.shields.io/pypi/v/django-templated-mail.svg 6 | :target: https://pypi.org/project/django-templated-mail 7 | 8 | .. image:: https://img.shields.io/travis/sunscrapers/django-templated-mail.svg 9 | :target: https://travis-ci.org/sunscrapers/django-templated-mail 10 | 11 | .. image:: https://img.shields.io/codecov/c/github/sunscrapers/django-templated-mail.svg 12 | :target: https://codecov.io/gh/sunscrapers/django-templated-mail 13 | 14 | .. image:: https://img.shields.io/scrutinizer/g/sunscrapers/django-templated-mail.svg 15 | :target: https://scrutinizer-ci.com/g/sunscrapers/django-templated-mail 16 | 17 | A simple wrapper for ``django.core.mail.EmailMultiAlternatives`` which makes 18 | use of Django template system to store email content in separate file. 19 | 20 | Developed by `SUNSCRAPERS `_ with passion & patience. 21 | 22 | Installation 23 | ============ 24 | 25 | Simply install using ``pip``: 26 | 27 | .. code-block:: bash 28 | 29 | $ pip install -U django-templated-mail 30 | 31 | Documentation 32 | ============= 33 | 34 | Documentation is available to study at 35 | `http://django-templated-mail.readthedocs.io `_ 36 | and in ``docs`` directory. 37 | 38 | Contributing and development 39 | ============================ 40 | 41 | To start developing on **django-templated-mail**, clone the repository: 42 | 43 | .. code-block:: bash 44 | 45 | $ git clone git@github.com:sunscrapers/django-templated-mail.git 46 | 47 | If you are a **pipenv** user you can quickly setup testing environment by 48 | using Make commands: 49 | 50 | .. code-block:: bash 51 | 52 | $ make init 53 | $ make test 54 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = django-templated-mail 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=django-templated-mail 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-templated-mail documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Oct 6 16:39:21 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'django-templated-mail' 50 | copyright = '2018, Sunscrapers' 51 | author = 'Sunscrapers' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '1.1' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '1.1.1' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = [] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | try: 87 | import sphinx_rtd_theme 88 | except: 89 | html_theme = 'default' 90 | else: 91 | html_theme = 'sphinx_rtd_theme' 92 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ['_static'] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # This is required for the alabaster theme 109 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 110 | html_sidebars = { 111 | '**': [ 112 | 'about.html', 113 | 'navigation.html', 114 | 'relations.html', # needs 'show_related': True theme option to display 115 | 'searchbox.html', 116 | 'donate.html', 117 | ] 118 | } 119 | 120 | 121 | # -- Options for HTMLHelp output ------------------------------------------ 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = 'django-templated-maildoc' 125 | 126 | 127 | # -- Options for LaTeX output --------------------------------------------- 128 | 129 | latex_elements = { 130 | # The paper size ('letterpaper' or 'a4paper'). 131 | # 132 | # 'papersize': 'letterpaper', 133 | 134 | # The font size ('10pt', '11pt' or '12pt'). 135 | # 136 | # 'pointsize': '10pt', 137 | 138 | # Additional stuff for the LaTeX preamble. 139 | # 140 | # 'preamble': '', 141 | 142 | # Latex figure (float) alignment 143 | # 144 | # 'figure_align': 'htbp', 145 | } 146 | 147 | # Grouping the document tree into LaTeX files. List of tuples 148 | # (source start file, target name, title, 149 | # author, documentclass [howto, manual, or own class]). 150 | latex_documents = [( 151 | master_doc, 'django-templated-mail.tex', 152 | 'django-templated-mail Documentation', 'Sunscrapers', 'manual' 153 | )] 154 | 155 | 156 | # -- Options for manual page output --------------------------------------- 157 | 158 | # One entry per manual page. List of tuples 159 | # (source start file, name, description, authors, manual section). 160 | man_pages = [( 161 | master_doc, 'django-templated-mail', 162 | 'django-templated-mail Documentation', [author], 1 163 | )] 164 | 165 | 166 | # -- Options for Texinfo output ------------------------------------------- 167 | 168 | # Grouping the document tree into Texinfo files. List of tuples 169 | # (source start file, target name, title, author, 170 | # dir menu entry, description, category) 171 | texinfo_documents = [( 172 | master_doc, 'django-templated-mail', 173 | 'django-templated-mail Documentation', author, 'django-templated-mail', 174 | 'One line description of project.', 'Miscellaneous' 175 | )] 176 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Supported Python versions 5 | ------------------------- 6 | 7 | * Python 2.7 8 | * Python 3.4 9 | * Python 3.5 10 | * Python 3.6 11 | 12 | Supported Django versions 13 | ------------------------- 14 | 15 | * Django 1.8 16 | * Django 1.11 17 | * Django 2.0 18 | 19 | Installation 20 | ------------ 21 | 22 | .. code-block:: bash 23 | 24 | $ pip install -U django-templated-mail 25 | 26 | No need to add it to your ``INSTALLED_APPS``. 27 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. django-templated-mail documentation master file, created by 2 | sphinx-quickstart on Fri Oct 6 16:39:21 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-templated-mail documentation! 7 | =============================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | getting_started 13 | settings 14 | templates_syntax 15 | sample_usage 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/source/sample_usage.rst: -------------------------------------------------------------------------------- 1 | Sample usage 2 | ============ 3 | 4 | At first let's discuss the simplest possible use case, where you just wish to 5 | send an email to a given address and using the given template. 6 | 7 | .. code-block:: python 8 | 9 | from templated_mail.mail import BaseEmailMessage 10 | 11 | BaseEmailMessage(template_name='email.html').send(to=['foo@bar.tld']) 12 | 13 | This one-liner will do all of the work required to render proper template blocks 14 | and assign the results to proper email pieces. It will also determine appropriate 15 | content type (including support for MIME) and send the output message to provided 16 | list of email address'. 17 | 18 | You might also wish to define your own subclass of 19 | ``templated_mail.mail.BaseEmailMessage`` to customize a thing or two. 20 | What might be most interesting for you is the ``get_context_data`` method, 21 | which returns context used during template rendering. 22 | 23 | .. code-block:: python 24 | 25 | class MyEmailMessage(BaseEmailMessage): 26 | def get_context_data(self): 27 | context = super(MyEmailMessage, self).get_context_data() 28 | context['foo'] = 'bar' 29 | return context 30 | 31 | You might also provide custom context data using the ``context`` parameter. 32 | 33 | .. code-block:: python 34 | 35 | from templated_mail.mail import BaseEmailMessage 36 | 37 | BaseEmailMessage(context={'foo': 'bar'}, template_name='email.html').send(to=['foo@bar.tld']) 38 | 39 | In other cases you might notice that some of your emails use common ``template_name`` 40 | and so to save some space you might wish to override the base class' attribute. 41 | 42 | .. code-block:: python 43 | 44 | class MyEmailMessage(BaseEmailMessage): 45 | template_name = 'email.html' 46 | -------------------------------------------------------------------------------- /docs/source/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | You may optionally provide following settings: 5 | 6 | .. code-block:: python 7 | 8 | 'DOMAIN': 'example.com' 9 | 'SITE_NAME': 'Foo Website' 10 | 11 | DOMAIN 12 | ------ 13 | 14 | Used in email template context. In most cases it is used to simplify building URLs, 15 | when frontend and backend are hosted under different address'. If not provided 16 | the current site's domain will be used. 17 | 18 | 19 | **Required**: ``False`` 20 | 21 | SITE_NAME 22 | --------- 23 | 24 | Used in email template context. Usually it will contain the desired title of your 25 | app. If not provided the current site's name will be used. 26 | 27 | 28 | **Default**: ``False`` 29 | -------------------------------------------------------------------------------- /docs/source/templates_syntax.rst: -------------------------------------------------------------------------------- 1 | Templates syntax 2 | ================ 3 | 4 | Email templates can be built using three simple blocks: 5 | 6 | - ``subject`` - used for subject of an email message 7 | - ``text_body`` - used for plaintext body of an email message (not required) 8 | - ``html_body`` - used for html body of an email message (not required) 9 | 10 | Examples 11 | -------- 12 | 13 | .. code-block:: html 14 | 15 | {% block subject %}Text and HTML mail subject{% endblock %} 16 | 17 | {% block text_body %}Foobar email content{% endblock %} 18 | 19 | .. code-block:: html 20 | 21 | {% block subject %}Text and HTML mail subject{% endblock %} 22 | 23 | {% block text_body %}Foobar email content{% endblock %} 24 | {% block html_body %}

Foobar email content

{% endblock %} 25 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/__init__.py -------------------------------------------------------------------------------- /examples/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/config/__init__.py -------------------------------------------------------------------------------- /examples/config/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = '()!xq2ufm%j@n^!v^p08&w87vvg)rk^=aoj8u1(4xho5iuh-)l' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | ALLOWED_HOSTS = [] 17 | 18 | 19 | # Application definition 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | 29 | 'templated_mail', 30 | 31 | 'simple', 32 | ] 33 | 34 | MIDDLEWARE = [ 35 | 'django.middleware.security.SecurityMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.middleware.common.CommonMiddleware', 38 | 'django.middleware.csrf.CsrfViewMiddleware', 39 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 40 | 'django.contrib.messages.middleware.MessageMiddleware', 41 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 42 | ] 43 | 44 | ROOT_URLCONF = 'config.urls' 45 | 46 | TEMPLATES = [ 47 | { 48 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 49 | 'DIRS': ['templates'], 50 | 'APP_DIRS': True, 51 | 'OPTIONS': { 52 | 'context_processors': [ 53 | 'django.template.context_processors.debug', 54 | 'django.template.context_processors.request', 55 | 'django.contrib.auth.context_processors.auth', 56 | 'django.contrib.messages.context_processors.messages', 57 | ], 58 | }, 59 | }, 60 | ] 61 | 62 | WSGI_APPLICATION = 'config.wsgi.application' 63 | 64 | 65 | # Database 66 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 67 | 68 | DATABASES = { 69 | 'default': { 70 | 'ENGINE': 'django.db.backends.sqlite3', 71 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 72 | } 73 | } 74 | 75 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 76 | 77 | # Password validation 78 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 79 | 80 | AUTH_PASSWORD_VALIDATORS = [ 81 | { 82 | 'NAME': 'django.contrib.auth.password_validation.' 83 | 'UserAttributeSimilarityValidator', 84 | }, 85 | { 86 | 'NAME': 'django.contrib.auth.password_validation.' 87 | 'MinimumLengthValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.' 91 | 'CommonPasswordValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.' 95 | 'NumericPasswordValidator', 96 | }, 97 | ] 98 | 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 102 | 103 | LANGUAGE_CODE = 'en-us' 104 | 105 | TIME_ZONE = 'UTC' 106 | 107 | USE_I18N = True 108 | 109 | USE_L10N = True 110 | 111 | USE_TZ = True 112 | 113 | 114 | # Static files (CSS, JavaScript, Images) 115 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 116 | 117 | STATIC_URL = '/static/' 118 | -------------------------------------------------------------------------------- /examples/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | url(r'^admin/', admin.site.urls), 6 | url(r'^simple/', include('simple.urls', namespace='simple')), 7 | ] 8 | -------------------------------------------------------------------------------- /examples/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 7 | from django.core.management import execute_from_command_line 8 | execute_from_command_line(sys.argv) 9 | -------------------------------------------------------------------------------- /examples/simple/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/simple/__init__.py -------------------------------------------------------------------------------- /examples/simple/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SimpleConfig(AppConfig): 5 | name = 'simple' 6 | -------------------------------------------------------------------------------- /examples/simple/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/examples/simple/migrations/__init__.py -------------------------------------------------------------------------------- /examples/simple/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from simple import views 4 | 5 | 6 | urlpatterns = [ 7 | url(r'^mail/txt_and_html', views.text_and_html_mail_view), 8 | url(r'^mail/txt', views.text_mail_view), 9 | url(r'^mail/html', views.html_mail_view), 10 | ] 11 | -------------------------------------------------------------------------------- /examples/simple/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from templated_mail.mail import BaseEmailMessage 4 | 5 | 6 | class TextEmailMessage(BaseEmailMessage): 7 | template_name = 'text_mail.html' 8 | 9 | 10 | class HTMLEmailMessage(BaseEmailMessage): 11 | template_name = 'html_mail.html' 12 | 13 | 14 | class TextAndHTMLEmailMessage(BaseEmailMessage): 15 | template_name = 'text_and_html_mail.html' 16 | 17 | 18 | def text_mail_view(request): 19 | recipients = ['foo@bar.tld'] 20 | TextEmailMessage(request).send(to=recipients) 21 | return HttpResponse('Text mail has been sent.') 22 | 23 | 24 | def html_mail_view(request): 25 | recipients = ['foo@bar.tld'] 26 | HTMLEmailMessage(request).send(to=recipients) 27 | return HttpResponse('HTML mail has been sent.') 28 | 29 | 30 | def text_and_html_mail_view(request): 31 | recipients = ['foo@bar.tld'] 32 | TextAndHTMLEmailMessage(request).send(to=recipients) 33 | return HttpResponse('Text and HTML mail has been sent.') 34 | -------------------------------------------------------------------------------- /examples/templates/html_mail.html: -------------------------------------------------------------------------------- 1 | {% block subject %}HTML mail subject{% endblock %} 2 | 3 | {% block html_body %}

Foobar email content

{% endblock %} -------------------------------------------------------------------------------- /examples/templates/text_and_html_mail.html: -------------------------------------------------------------------------------- 1 | {% block subject %}Text and HTML mail subject{% endblock %} 2 | 3 | {% block text_body %}Foobar email content{% endblock %} 4 | {% block html_body %}

Foobar email content

{% endblock %} -------------------------------------------------------------------------------- /examples/templates/text_mail.html: -------------------------------------------------------------------------------- 1 | {% block subject %}Text mail subject{% endblock %} 2 | 3 | {% block text_body %}Foobar email content{% endblock %} -------------------------------------------------------------------------------- /requirements/base: -------------------------------------------------------------------------------- 1 | django>=1.8 2 | -------------------------------------------------------------------------------- /requirements/test: -------------------------------------------------------------------------------- 1 | coverage==4.4.1 2 | flake8==3.4.1 3 | pytest==3.2.2 4 | pytest-cov==2.5.1 5 | pytest-django==3.1.2 6 | pytest-pythonpath==0.7.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | 7 | with open('README.rst', 'r') as f: 8 | readme = f.read() 9 | 10 | 11 | def get_packages(package): 12 | return [ 13 | dirpath for dirpath, dirnames, filenames in os.walk(package) 14 | if os.path.exists(os.path.join(dirpath, '__init__.py')) 15 | ] 16 | 17 | 18 | setup( 19 | name='django-templated-mail', 20 | version='1.1.1', 21 | packages=get_packages('templated_mail'), 22 | license='MIT', 23 | author='Sunscrapers', 24 | description='Send emails using Django template system.', 25 | author_email='info@sunscrapers.com', 26 | long_description=readme, 27 | install_requires=[], 28 | include_package_data=True, 29 | url='https://github.com/sunscrapers/django-templated-mail', 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Framework :: Django :: 1.11', 33 | 'Framework :: Django :: 2.0', 34 | 'Framework :: Django :: 2.1', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python', 39 | 'Programming Language :: Python :: 2.7', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Programming Language :: Python :: 3.5', 42 | 'Programming Language :: Python :: 3.6', 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /templated_mail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/templated_mail/__init__.py -------------------------------------------------------------------------------- /templated_mail/mail.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.sites.shortcuts import get_current_site 3 | from django.core import mail 4 | from django.template.context import make_context 5 | from django.template.loader import get_template 6 | from django.template.loader_tags import BlockNode, ExtendsNode 7 | from django.views.generic.base import ContextMixin 8 | 9 | 10 | class BaseEmailMessage(mail.EmailMultiAlternatives, ContextMixin): 11 | _node_map = { 12 | 'subject': 'subject', 13 | 'text_body': 'body', 14 | 'html_body': 'html', 15 | } 16 | template_name = None 17 | 18 | def __init__(self, request=None, context=None, template_name=None, 19 | *args, **kwargs): 20 | super(BaseEmailMessage, self).__init__(*args, **kwargs) 21 | 22 | self.request = request 23 | self.context = {} if context is None else context 24 | self.html = None 25 | 26 | if template_name is not None: 27 | self.template_name = template_name 28 | 29 | def get_context_data(self, **kwargs): 30 | ctx = super(BaseEmailMessage, self).get_context_data(**kwargs) 31 | context = dict(ctx, **self.context) 32 | if self.request: 33 | site = get_current_site(self.request) 34 | domain = context.get('domain') or ( 35 | getattr(settings, 'DOMAIN', '') or site.domain 36 | ) 37 | protocol = context.get('protocol') or ( 38 | 'https' if self.request.is_secure() else 'http' 39 | ) 40 | site_name = context.get('site_name') or ( 41 | getattr(settings, 'SITE_NAME', '') or site.name 42 | ) 43 | user = context.get('user') or self.request.user 44 | else: 45 | domain = context.get('domain') or getattr(settings, 'DOMAIN', '') 46 | protocol = context.get('protocol') or 'http' 47 | site_name = context.get('site_name') or getattr( 48 | settings, 'SITE_NAME', '' 49 | ) 50 | user = context.get('user') 51 | 52 | context.update({ 53 | 'domain': domain, 54 | 'protocol': protocol, 55 | 'site_name': site_name, 56 | 'user': user 57 | }) 58 | return context 59 | 60 | def render(self): 61 | context = make_context(self.get_context_data(), request=self.request) 62 | template = get_template(self.template_name) 63 | with context.bind_template(template.template): 64 | blocks = self._get_blocks(template.template.nodelist, context) 65 | for block_node in blocks.values(): 66 | self._process_block(block_node, context) 67 | self._attach_body() 68 | 69 | def send(self, to, *args, **kwargs): 70 | self.render() 71 | 72 | self.to = to 73 | self.cc = kwargs.pop('cc', []) 74 | self.bcc = kwargs.pop('bcc', []) 75 | self.reply_to = kwargs.pop('reply_to', []) 76 | self.from_email = kwargs.pop( 77 | 'from_email', settings.DEFAULT_FROM_EMAIL 78 | ) 79 | 80 | super(BaseEmailMessage, self).send(*args, **kwargs) 81 | 82 | def _process_block(self, block_node, context): 83 | attr = self._node_map.get(block_node.name) 84 | if attr is not None: 85 | setattr(self, attr, block_node.render(context).strip()) 86 | 87 | def _get_blocks(self, nodelist, context): 88 | blocks = {} 89 | for node in nodelist: 90 | if isinstance(node, ExtendsNode): 91 | parent = node.get_parent(context) 92 | blocks.update(self._get_blocks(parent.nodelist, context)) 93 | blocks.update({ 94 | node.name: node for node in nodelist.get_nodes_by_type(BlockNode) 95 | }) 96 | return blocks 97 | 98 | def _attach_body(self): 99 | if self.body and self.html: 100 | self.attach_alternative(self.html, 'text/html') 101 | elif self.html: 102 | self.body = self.html 103 | self.content_subtype = 'html' 104 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-templated-mail/65e5a34f69d50d4d1b6acdd337f9efcc44b32fae/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.base import ContextMixin 2 | from templated_mail.mail import BaseEmailMessage 3 | 4 | 5 | class MockMailContext(ContextMixin): 6 | def get_context_data(self, **kwargs): 7 | context = super(MockMailContext, self).get_context_data(**kwargs) 8 | context['thing'] = 42 9 | return context 10 | 11 | 12 | class MockMail(BaseEmailMessage, MockMailContext): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = 'quentin-tarantino' 6 | 7 | DEBUG = False 8 | 9 | ALLOWED_HOSTS = [] 10 | 11 | INSTALLED_APPS = [ 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 19 | 'tests', 20 | ] 21 | 22 | MIDDLEWARE = [ 23 | 'django.middleware.security.SecurityMiddleware', 24 | 'django.contrib.sessions.middleware.SessionMiddleware', 25 | 'django.middleware.common.CommonMiddleware', 26 | 'django.middleware.csrf.CsrfViewMiddleware', 27 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 28 | 'django.contrib.messages.middleware.MessageMiddleware', 29 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 30 | ] 31 | 32 | TEMPLATES = [ 33 | { 34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 35 | 'DIRS': ['templates'], 36 | 'APP_DIRS': True, 37 | 'OPTIONS': { 38 | 'context_processors': [ 39 | 'django.template.context_processors.debug', 40 | 'django.template.context_processors.request', 41 | 'django.contrib.auth.context_processors.auth', 42 | 'django.contrib.messages.context_processors.messages', 43 | ], 44 | }, 45 | }, 46 | ] 47 | 48 | DATABASES = { 49 | 'default': { 50 | 'ENGINE': 'django.db.backends.sqlite3', 51 | 'NAME': ':memory:', 52 | } 53 | } 54 | 55 | LANGUAGE_CODE = 'en-us' 56 | 57 | TIME_ZONE = 'UTC' 58 | 59 | USE_I18N = True 60 | 61 | USE_L10N = True 62 | 63 | USE_TZ = True 64 | 65 | STATIC_URL = '/static/' 66 | -------------------------------------------------------------------------------- /tests/templates/extends.html: -------------------------------------------------------------------------------- 1 | {% extends "text_and_html_mail.html" %} 2 | 3 | {% block html_body %}Some extended HTML body{% endblock html_body %} 4 | -------------------------------------------------------------------------------- /tests/templates/html_mail.html: -------------------------------------------------------------------------------- 1 | {% block subject %}HTML mail subject{% endblock %} 2 | 3 | {% block html_body %}

Foobar email content

{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/nested_extends.html: -------------------------------------------------------------------------------- 1 | {% extends "extends.html" %} 2 | 3 | {% block text_body %}Some extended text body{% endblock text_body %} 4 | -------------------------------------------------------------------------------- /tests/templates/text_and_html_mail.html: -------------------------------------------------------------------------------- 1 | {% block subject %}Text and HTML mail subject{% endblock %} 2 | 3 | {% block text_body %}Foobar email content{% endblock %} 4 | {% block html_body %}

Foobar email content

{% endblock %} -------------------------------------------------------------------------------- /tests/templates/text_mail.html: -------------------------------------------------------------------------------- 1 | {% block subject %}Text mail subject{% endblock %} 2 | 3 | {% block text_body %}Foobar email content{% endblock %} -------------------------------------------------------------------------------- /tests/test_mail.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest import mock 3 | except ImportError: 4 | import mock 5 | 6 | from django.contrib.auth.models import AnonymousUser 7 | from django.contrib.sites.shortcuts import get_current_site 8 | from django.core import mail 9 | from django.test import override_settings, RequestFactory, TestCase 10 | from templated_mail.mail import BaseEmailMessage 11 | 12 | from .helpers import MockMail 13 | 14 | 15 | class TestBaseEmailMessage(TestCase): 16 | def setUp(self): 17 | self.factory = RequestFactory() 18 | self.recipients = ['foo@bar.tld'] 19 | 20 | @mock.patch('django.core.handlers.wsgi.WSGIRequest.is_secure') 21 | def test_get_context_data_with_insecure_request(self, is_secure_mock): 22 | is_secure_mock.return_value = False 23 | 24 | request = self.factory.get('/') 25 | request.user = AnonymousUser() 26 | 27 | email_message = BaseEmailMessage( 28 | request=request, template_name='text_mail.html' 29 | ) 30 | context = email_message.get_context_data() 31 | site = get_current_site(request) 32 | 33 | self.assertEquals(context['domain'], site.domain) 34 | self.assertEquals(context['protocol'], 'http') 35 | self.assertEquals(context['site_name'], site.name) 36 | self.assertEquals(context['user'], request.user) 37 | 38 | @mock.patch('django.core.handlers.wsgi.WSGIRequest.is_secure') 39 | def test_get_context_data_with_secure_request(self, is_secure_mock): 40 | is_secure_mock.return_value = True 41 | 42 | request = self.factory.get('/') 43 | request.user = AnonymousUser() 44 | 45 | email_message = BaseEmailMessage( 46 | request=request, template_name='text_mail.html' 47 | ) 48 | context = email_message.get_context_data() 49 | site = get_current_site(request) 50 | 51 | self.assertEquals(context['domain'], site.domain) 52 | self.assertEquals(context['protocol'], 'https') 53 | self.assertEquals(context['site_name'], site.name) 54 | self.assertEquals(context['user'], request.user) 55 | 56 | def test_get_context_data_without_request_no_context(self): 57 | email_message = BaseEmailMessage(template_name='text_mail.html') 58 | context = email_message.get_context_data() 59 | 60 | self.assertEquals(context['domain'], '') 61 | self.assertEquals(context['protocol'], 'http') 62 | self.assertEquals(context['site_name'], '') 63 | self.assertEquals(context['user'], None) 64 | 65 | def test_get_context_data_without_request_user_context(self): 66 | user = AnonymousUser() 67 | email_message = BaseEmailMessage( 68 | context={'user': user}, template_name='text_mail.html' 69 | ) 70 | context = email_message.get_context_data() 71 | 72 | self.assertEquals(context['domain'], '') 73 | self.assertEquals(context['protocol'], 'http') 74 | self.assertEquals(context['site_name'], '') 75 | self.assertEquals(context['user'], user) 76 | 77 | def test_text_mail_contains_valid_data(self): 78 | request = self.factory.get('/') 79 | request.user = AnonymousUser() 80 | 81 | BaseEmailMessage( 82 | request=request, template_name='text_mail.html' 83 | ).send(to=self.recipients) 84 | 85 | self.assertEqual(len(mail.outbox), 1) 86 | self.assertEqual(mail.outbox[0].recipients(), self.recipients) 87 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject') 88 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 89 | self.assertEqual(mail.outbox[0].alternatives, []) 90 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 91 | 92 | def test_html_mail_contains_valid_data(self): 93 | request = self.factory.get('/') 94 | request.user = AnonymousUser() 95 | 96 | BaseEmailMessage( 97 | request=request, template_name='html_mail.html' 98 | ).send(to=self.recipients) 99 | 100 | self.assertEqual(len(mail.outbox), 1) 101 | self.assertEqual(mail.outbox[0].recipients(), self.recipients) 102 | self.assertEqual(mail.outbox[0].subject, 'HTML mail subject') 103 | self.assertEqual(mail.outbox[0].body, '

Foobar email content

') 104 | self.assertEqual(mail.outbox[0].alternatives, []) 105 | self.assertEqual(mail.outbox[0].content_subtype, 'html') 106 | 107 | def test_text_and_html_mail_contains_valid_data(self): 108 | request = self.factory.get('/') 109 | request.user = AnonymousUser() 110 | 111 | BaseEmailMessage( 112 | request=request, template_name='text_and_html_mail.html' 113 | ).send(to=self.recipients) 114 | 115 | self.assertEqual(len(mail.outbox), 1) 116 | self.assertEqual(mail.outbox[0].recipients(), self.recipients) 117 | self.assertEqual(mail.outbox[0].subject, 'Text and HTML mail subject') 118 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 119 | self.assertEqual( 120 | mail.outbox[0].alternatives, 121 | [('

Foobar email content

', 'text/html')] 122 | ) 123 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 124 | 125 | def test_can_send_mail_with_none_request(self): 126 | BaseEmailMessage( 127 | request=None, template_name='text_mail.html' 128 | ).send(to=self.recipients) 129 | 130 | self.assertEqual(len(mail.outbox), 1) 131 | self.assertEqual(mail.outbox[0].recipients(), self.recipients) 132 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject') 133 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 134 | self.assertEqual(mail.outbox[0].alternatives, []) 135 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 136 | 137 | def test_mail_cc_is_sent_to_valid_cc(self): 138 | request = self.factory.get('/') 139 | request.user = AnonymousUser() 140 | 141 | cc = ['email@example.tld'] 142 | 143 | BaseEmailMessage( 144 | request=request, template_name='text_mail.html' 145 | ).send(to=self.recipients, cc=cc) 146 | 147 | self.assertEqual(len(mail.outbox), 1) 148 | self.assertEqual(mail.outbox[0].to, self.recipients) 149 | self.assertEqual(mail.outbox[0].cc, cc) 150 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject') 151 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 152 | self.assertEqual(mail.outbox[0].alternatives, []) 153 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 154 | 155 | def test_mail_bcc_is_sent_to_valid_bcc(self): 156 | request = self.factory.get('/') 157 | request.user = AnonymousUser() 158 | 159 | bcc = ['email@example.tld'] 160 | 161 | BaseEmailMessage( 162 | request=request, template_name='text_mail.html' 163 | ).send(to=self.recipients, bcc=bcc) 164 | 165 | self.assertEqual(len(mail.outbox), 1) 166 | self.assertEqual(mail.outbox[0].to, self.recipients) 167 | self.assertEqual(mail.outbox[0].bcc, bcc) 168 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject') 169 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 170 | self.assertEqual(mail.outbox[0].alternatives, []) 171 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 172 | 173 | def test_mail_reply_to_is_sent_with_valid_reply_to(self): 174 | request = self.factory.get('/') 175 | request.user = AnonymousUser() 176 | 177 | reply_to = ['email@example.tld'] 178 | 179 | BaseEmailMessage( 180 | request=request, template_name='text_mail.html' 181 | ).send(to=self.recipients, reply_to=reply_to) 182 | 183 | self.assertEqual(len(mail.outbox), 1) 184 | self.assertEqual(mail.outbox[0].to, self.recipients) 185 | self.assertEqual(mail.outbox[0].reply_to, reply_to) 186 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject') 187 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 188 | self.assertEqual(mail.outbox[0].alternatives, []) 189 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 190 | 191 | def test_mail_from_email_is_sent_with_valid_from_email(self): 192 | request = self.factory.get('/') 193 | request.user = AnonymousUser() 194 | 195 | from_email = '' 196 | 197 | BaseEmailMessage( 198 | request=request, template_name='text_mail.html' 199 | ).send(to=self.recipients, from_email=from_email) 200 | 201 | self.assertEqual(len(mail.outbox), 1) 202 | self.assertEqual(mail.outbox[0].to, self.recipients) 203 | self.assertEqual(mail.outbox[0].from_email, from_email) 204 | self.assertEqual(mail.outbox[0].subject, 'Text mail subject') 205 | self.assertEqual(mail.outbox[0].body, 'Foobar email content') 206 | self.assertEqual(mail.outbox[0].alternatives, []) 207 | self.assertEqual(mail.outbox[0].content_subtype, 'plain') 208 | 209 | @override_settings(DEFAULT_FROM_EMAIL='default@example.tld') 210 | def test_mail_without_from_email_is_sent_with_valid_from_email(self): 211 | BaseEmailMessage( 212 | request=None, template_name='text_mail.html' 213 | ).send(to=self.recipients) 214 | 215 | self.assertEqual(len(mail.outbox), 1) 216 | self.assertEqual(mail.outbox[0].to, self.recipients) 217 | self.assertEqual(mail.outbox[0].from_email, 'default@example.tld') 218 | 219 | def test_extending_mail_with_context_mixin(self): 220 | email_message = MockMail( 221 | template_name='text_mail.html', context={'foo': 'bar'} 222 | ) 223 | 224 | context = email_message.get_context_data() 225 | 226 | self.assertEquals(context['foo'], 'bar') 227 | self.assertEquals(context['thing'], 42) 228 | 229 | def test_extending_mail_template(self): 230 | email_message = BaseEmailMessage(template_name='extends.html') 231 | email_message.render() 232 | 233 | self.assertEquals(email_message.subject, 'Text and HTML mail subject') 234 | self.assertEquals(email_message.body, 'Foobar email content') 235 | self.assertEquals(email_message.html, 'Some extended HTML body') 236 | 237 | def text_nested_extending_mail_template(self): 238 | email_message = BaseEmailMessage(template_name='nested_extends.html') 239 | email_message.render() 240 | 241 | self.assertEquals(email_message.subject, 'Text and HTML mail subject') 242 | self.assertEquals(email_message.body, 'Some extended text body') 243 | self.assertEquals(email_message.html, 'Some extended HTML body') 244 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_paths = . 3 | 4 | [tox] 5 | envlist = 6 | py{27,34,35,36,37}-django111 7 | py{34,35,36,37}-django20 8 | py{35,36,37}-django{21,master} 9 | flake8 10 | 11 | 12 | [travis:env] 13 | DJANGO = 14 | 1.11: django111 15 | 2.0: django20 16 | 2.1: django21 17 | master: djangomaster 18 | 19 | 20 | [testenv] 21 | passenv = CI TRAVIS TRAVIS_* 22 | basepython = 23 | py27: python2.7 24 | py34: python3.4 25 | py35: python3.5 26 | py36: python3.6 27 | py37: python3.7 28 | deps = 29 | django111: django>=1.11,<2.0 30 | django20: django>=2.0,<2.1 31 | django21: django>=2.1,<2.2 32 | djangomaster: git+git://github.com/django/django.git 33 | py27: mock 34 | -rrequirements/test 35 | commands = 36 | py.test --ds=tests.settings --capture=no --cov-report term-missing --cov-report html --cov=templated_mail tests 37 | 38 | 39 | [testenv:flake8] 40 | basepython = python3 41 | skip_install = true 42 | deps = flake8 43 | commands = flake8 . --------------------------------------------------------------------------------