pax_global_header00006660000000000000000000000064135420632150014513gustar00rootroot0000000000000052 comment=dfab5c268fe3729863667f3a69c84ae45892ce28 django-mailer-2.0/000077500000000000000000000000001354206321500140655ustar00rootroot00000000000000django-mailer-2.0/.coveragerc000066400000000000000000000000701354206321500162030ustar00rootroot00000000000000[run] source = mailer omit = mailer/tests.py branch = 1 django-mailer-2.0/.gitignore000066400000000000000000000001261354206321500160540ustar00rootroot00000000000000dist/ build/ django_mailer.egg-info/ htmlcov/ .tox/ MANIFEST .coverage *.mo *~ *.pyc django-mailer-2.0/.travis.yml000066400000000000000000000015421354206321500162000ustar00rootroot00000000000000language: python python: 2.7 env: - TOXENV=checkmanifest matrix: include: - python: 2.7 env: TOXENV=py27-django111-test - python: 2.7 env: TOXENV=py27-flake - python: 3.4 env: TOXENV=py34-django111-test - python: 3.4 env: TOXENV=py34-django20-test - python: 3.4 env: TOXENV=py34-flake - python: 3.5 env: TOXENV=py35-django111-test - python: 3.5 env: TOXENV=py35-django20-test - python: 3.5 env: TOXENV=py35-flake - python: 3.6 env: TOXENV=py36-django111-test - python: 3.6 env: TOXENV=py36-django20-test - python: 3.6 env: TOXENV=py36-django21-test - python: 3.6 env: TOXENV=py36-django22-test - python: 3.6 env: TOXENV=py36-django30-test - python: 3.6 env: TOXENV=py36-flake install: - pip install coveralls tox>=2.1 script: - tox after_script: - coveralls sudo: false django-mailer-2.0/AUTHORS000066400000000000000000000003051354206321500151330ustar00rootroot00000000000000 The PRIMARY AUTHORS are: * James Tauber * Brian Rosner ADDITIONAL CONTRIBUTORS include: * Michael Trier * Doug Napoleone * Jannis Leidel * Luke Plant * Renato Alves django-mailer-2.0/CHANGES.rst000066400000000000000000000065541354206321500157010ustar00rootroot00000000000000Change log ========== 2.0 - 2019-09-23 ---------------- * Django 3.0 support * Dropped support for old Django versions (before 1.11) * Changed DB ``priority`` field to an integer, instead of text field container an integer * Multi-process safety for sending emails via database row-level locking. Previously, there was a file-system based lock to ensure that multiple processes were not attempting to send the mail queue, to stop multiple sending of the same email. However, this mechanism only works if all processes that might be attempting to do this are on the same machine with access to the same file-system. Now, in addition to this file lock, we use transactions and row-level locking in the database when attempting to send a message, which guarantees that only one process can send the message. In addition, for databases that support ``NOWAIT`` with ``SELECT FOR UPDATE``, such as PostgreSQL, if multiple processes attempt to send the mail queue at the same time, the work should be distributed between them (rather than being done by only one process). A negative consequence is that **SQLite support is degraded**: due to the way it implements locking and our use of transactions when sending the email queue, you can get exceptions in other processes that are trying to add items to the queue. Use of SQLite with django-mailer is **not recommended**. * ``retry_deferred`` command has also been updated to be simpler and work correctly for multiple processes. * Dropped some backwards compat support for Django < 1.8. If you are upgrading from a version of Django before 1.8, you should install a version of django-mailer < 2.0, do ``send_all`` to flush the queue, then upgrade django-mailer to 2.0 or later. 1.2.6 - 2019-04-03 ------------------ * Official Django 2.1 and 2.2 support. * Don't close DB connection in management commands. This is unnecessary with modern Django. 1.2.5 ----- * Fixed packaging file permission problems. * Added Japanese locale (thanks msk7777) 1.2.4 ----- * Django 2.0 support. 1.2.3 ----- * Fixed crasher with models ``__str__`` 1.2.2 ----- * Django 1.10 support. * Fixed reprs for Message and MessageLog. 1.2.1 ----- * More helpful admin for Message and MessageLog * Handle exceptions from really old Django versions 1.2.0 ----- * Save the ``Message-ID`` header on ``Message`` explicitly to enable finding emails using this identifier. This includes a database schema migration. 1.1.0 ----- * Deprecated calling ``send_mail`` and ``send_html_mail`` using ``priority`` kwargs ``"high"``, ``"medium"``, and ``"low"``. Instead you should use ``PRIORITY_HIGH``, ``PRIORITY_MEDIUM`` and ``PRIORITY_LOW`` from ``mailer.models``. * Fixed bug with migrations for Django 1.7, which wanted to create a migration to 'fix' the EmailField length back down to 75 instead of 254. 1.0.1 ----- * Included migrations - for both South and Django 1.7 native migrations. Note: * If you use South, you will need at least South 1.0 * You will need to use '--fake' or '--fake-initial' on existing installations. These migrations were supposed to be in 1.0.0 but were omitted due to a packaging error. 1.0.0 ----- * Throttling of email sending * Django 1.8 support * Admin tweaks and improvements * Various other fixes, especially from Renato Alves - thank you! 0.1.0 ----- * First PyPI version django-mailer-2.0/CONTRIBUTING.rst000066400000000000000000000052731354206321500165350ustar00rootroot00000000000000How to Contribute ================= There are many ways you can help contribute to django-mailer and the various apps, themes, and starter projects that it is made up of. Contributing code, writing documentation, reporting bugs, as well as reading and providing feedback on issues and pull requests, all are valid and necessary ways to help. Local development setup ----------------------- To set up your environment to be able to work on django-mailer, do the following: 1. Fork the django-mailer repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/django-mailer.git $ cd django-mailer/ 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper:: $ mkvirtualenv django-mailer $ python setup.py develop 4. Install test requirements:: $ pip install coverage mock 5. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature 6. Now you can make your changes locally. Run the tests in the virtualenv using:: $ ./runtests.py To run the tests in all supported environments, do:: $ pip install tox $ tox 7. When your changes are done, push the branch to GitHub, and create a pull request. Coding style ------------ When writing code to be included in django-mailer keep our style in mind: * Follow `PEP8 `_ . There are some cases where we do not follow PEP8 but it is an excellent starting point. * Follow `Django's coding style `_ we're pretty much in agreement on Django style outlined there. Pull Requests ------------- Please keep your pull requests focused on one specific thing only. If you have a number of contributions to make, then please send seperate pull requests. It is much easier on maintainers to receive small, well defined, pull requests, than it is to have a single large one that batches up a lot of unrelated commits. If you ended up making multiple commits for one logical change, please rebase into a single commit:: git rebase -i HEAD~10 # where 10 is the number of commits back you need This will pop up an editor with your commits and some instructions you want to squash commits down by replacing 'pick' with 's' to have it combined with the commit before it. You can squash multiple ones at the same time. When you save and exit the text editor where you were squashing commits, git will squash them down and then present you with another editor with commit messages. Choose the one to apply to the squashed commit (or write a new one entirely.) Save and exit will complete the rebase. Use a forced push to your fork:: git push -f django-mailer-2.0/LICENSE000066400000000000000000000020661354206321500150760ustar00rootroot00000000000000Copyright (c) 2009-2014 James Tauber and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-mailer-2.0/MANIFEST.in000066400000000000000000000003461354206321500156260ustar00rootroot00000000000000include AUTHORS include LICENSE include *.rst include MANIFEST.in include .coveragerc include tox.ini .travis.yml include *.py include *.sh graft docs graft src graft tests global-exclude *.py[cod] __pycache__/* *.so *.dylib *~ django-mailer-2.0/README.rst000066400000000000000000000103611354206321500155550ustar00rootroot00000000000000Django Mailer ------------- .. image:: http://slack.pinaxproject.com/badge.svg :target: http://slack.pinaxproject.com/ .. image:: https://img.shields.io/travis/pinax/django-mailer.svg :target: https://travis-ci.org/pinax/django-mailer .. image:: https://img.shields.io/coveralls/pinax/django-mailer.svg :target: https://coveralls.io/r/pinax/django-mailer .. image:: https://img.shields.io/pypi/dm/django-mailer.svg :target: https://pypi.python.org/pypi/django-mailer/ .. image:: https://img.shields.io/pypi/v/django-mailer.svg :target: https://pypi.python.org/pypi/django-mailer/ .. image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://pypi.python.org/pypi/django-mailer/ Pinax ----- Pinax is an open-source platform built on the Django Web Framework. It is an ecosystem of reusable Django apps, themes, and starter project templates. This collection can be found at http://pinaxproject.com. This app was developed as part of the Pinax ecosystem but is just a Django app and can be used independently of other Pinax apps. django-mailer ------------- ``django-mailer`` is a reusable Django app for queuing the sending of email. Requirements ------------ * Django >= 1.11 * Databases: django-mailer supports all databases that Django supports, with the following notes: * SQLite: you may experience 'database is locked' errors if the ``send_mail`` command runs when anything else is attempting to put items on the queue. For this reason SQLite is not recommended for use with django-mailer. Getting Started --------------- Simple usage instructions: In ``settings.py``: :: INSTALLED_APPS = [ ... "mailer", ... ] EMAIL_BACKEND = "mailer.backend.DbBackend" Run database migrations to set up the needed database tables. Then send email in the normal way, as per the `Django email docs `_, and they will be added to the queue. To actually send the messages on the queue, add this to a cron job file or equivalent:: * * * * * (/path/to/your/python /path/to/your/manage.py send_mail >> ~/cron_mail.log 2>&1) 0,20,40 * * * * (/path/to/your/python /path/to/your/manage.py retry_deferred >> ~/cron_mail_deferred.log 2>&1) To prevent from the database filling up with the message log, you should clean it up every once in a while. To remove successful log entries older than a week, add this to a cron job file or equivalent:: 0 0 * * * (/path/to/your/python /path/to/your/manage.py purge_mail_log 7 >> ~/cron_mail_purge.log 2>&1) Documentation ------------- See ``usage.rst`` in the docs for more advanced use cases - https://github.com/pinax/django-mailer/blob/master/docs/usage.rst#usage. The Pinax documentation is available at http://pinaxproject.com/pinax/. Contribute ---------- See ``CONTRIBUTING.rst`` for information about contributing patches to ``django-mailer``. See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/ including a video, or our How to Contribute (http://pinaxproject.com/pinax/how_to_contribute/) section for an overview on how contributing to Pinax works. For concrete contribution ideas, please see our Ways to Contribute/What We Need Help With (http://pinaxproject.com/pinax/ways_to_contribute/) section. In case of any questions we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). Code of Conduct --------------- In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/. We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. Pinax Project Blog and Twitter ------------------------------ For updates and news regarding the Pinax Project, please follow us on Twitter at @pinaxproject and check out our blog http://blog.pinaxproject.com. django-mailer-2.0/RELEASE.rst000066400000000000000000000012061354206321500156760ustar00rootroot00000000000000Release process --------------- * Check that the master branching is passing all tests - https://travis-ci.org/pinax/django-mailer * In CHANGES.rst, change the 'Unreleased' heading to the new version, and commit. * Change the version in mailer/__init__.py, removing ``.dev1`` if necessary, and in setup.py, and commit. * Release:: $ ./release.sh * Tag the release e.g.:: $ git tag 1.2.0 $ git push upstream master --tags Post release ------------ * Add new section 'Unreleased' section at top of CHANGES.rst * Bump version in mailer/__init__.py (to what it is most likely to be next), and in setup.py including ``.dev1``. django-mailer-2.0/docs/000077500000000000000000000000001354206321500150155ustar00rootroot00000000000000django-mailer-2.0/docs/index.rst000066400000000000000000000001171354206321500166550ustar00rootroot00000000000000 ============= django-mailer ============= Contents: .. toctree:: usage django-mailer-2.0/docs/usage.rst000066400000000000000000000136411354206321500166600ustar00rootroot00000000000000===== Usage ===== First, add "mailer" to your ``INSTALLED_APPS`` in your settings.py. Run ``./manage.py migrate`` to install models. Using EMAIL_BACKEND =================== This is the preferred and easiest way to use django-mailer. To automatically switch all your mail to use django-mailer, first set EMAIL_BACKEND:: EMAIL_BACKEND = "mailer.backend.DbBackend" If you were previously using a non-default EMAIL_BACKEND, you need to configure the MAILER_EMAIL_BACKEND setting, so that django-mailer knows how to actually send the mail:: MAILER_EMAIL_BACKEND = "your.actual.EmailBackend" Now, just use the normal `Django mail functions `_ for sending email. These functions will store mail on a queue in the database, which must be sent as below. Explicitly putting mail on the queue ==================================== If you don't want to send all email through django-mailer, you can send mail using ``mailer.send_mail``, which has the same signature as Django's ``send_mail`` function. You can also do the following:: # favour django-mailer but fall back to django.core.mail from django.conf import settings if "mailer" in settings.INSTALLED_APPS: from mailer import send_mail else: from django.core.mail import send_mail and then just call send_mail like you normally would in Django:: send_mail(subject, message_body, settings.DEFAULT_FROM_EMAIL, recipients) There is also a convenience function ``mailer.send_html_mail`` for creating HTML (this function is **not** in Django):: send_html_mail(subject, message_plaintext, message_html, settings.DEFAULT_FROM_EMAIL, recipients) Additionally you can send all the admins as specified in the ``ADMIN`` setting by calling:: mail_admins(subject, message_body) or all managers as defined in the ``MANAGERS`` setting by calling:: mail_managers(subject, message_body) Clear queue with command extensions =================================== With mailer in your INSTALLED_APPS, there will be three new manage.py commands you can run: * ``send_mail`` will clear the current message queue. If there are any failures, they will be marked deferred and will not be attempted again by ``send_mail``. * ``retry_deferred`` will move any deferred mail back into the normal queue (so it will be attempted again on the next ``send_mail``). * ``purge_mail_log`` will remove old successful message logs from the database, to prevent it from filling up your database You may want to set these up via cron to run regularly:: * * * * * (/path/to/your/python /path/to/your/manage.py send_mail >> ~/cron_mail.log 2>&1) 0,20,40 * * * * (/path/to/your/python /path/to/your/manage.py retry_deferred >> ~/cron_mail_deferred.log 2>&1) 0 0 * * * (/path/to/your/python /path/to/your/manage.py purge_mail_log 7 >> ~/cron_mail_purge.log 2>&1) For use in Pinax, for example, that might look like:: * * * * * (cd $PINAX; /usr/local/bin/python2.5 manage.py send_mail >> $PINAX/cron_mail.log 2>&1) 0,20,40 * * * * (cd $PINAX; /usr/local/bin/python2.5 manage.py retry_deferred >> $PINAX/cron_mail_deferred.log 2>&1) 0 0 * * * (cd $PINAX; /usr/local/bin/python2.5 manage.py purge_mail_log 7 >> $PINAX/cron_mail_purge.log 2>&1) This attempts to send mail every minute with a retry on failure every 20 minutes, and purges the mail log for entries older than 7 days. ``manage.py send_mail`` uses a lock file in case clearing the queue takes longer than the interval between calling ``manage.py send_mail``. Note that if your project lives inside a virtualenv, you also have to execute this command from the virtualenv. The same, naturally, applies also if you're executing it with cron. The `Pinax documentation`_ explains that in more details. .. _pinax documentation: http://pinaxproject.com/docs/dev/deployment.html#sending-mail-and-notices Controlling the delivery process ================================ If you wish to have a finer control over the delivery process, which defaults to deliver everything in the queue, you can use the following 3 variables (default values shown):: MAILER_EMAIL_MAX_BATCH = None # integer or None MAILER_EMAIL_MAX_DEFERRED = None # integer or None MAILER_EMAIL_THROTTLE = 0 # passed to time.sleep() These control how many emails are sent successfully before stopping the current run `MAILER_EMAIL_MAX_BATCH`, after how many failed/deferred emails should it stop `MAILER_EMAIL_MAX_DEFERRED` and how much time to wait between each email `MAILER_EMAIL_THROTTLE`. Unprocessed emails will be evaluated in the following delivery iterations. Other settings ============== If you need to be able to control where django-mailer puts its lock file (used to ensure mail is not sent twice), you can set ``MAILER_LOCK_PATH`` to a full absolute path to the file to be used as a lock. The extension ".lock" will be added. The process running ``send_mail`` needs to have permissions to create and delete this file, and others in the same directory. With the default value of ``None`` django-mailer will use a path in current working directory. If you need to change the batch size used by django-mailer to save messages in ``mailer.backend.DbBackend``, you can set ``MAILER_MESSAGES_BATCH_SIZE`` to a value more suitable for you. This value, which defaults to `None`, will be passed to `Django's bulk_create method `_ as the `batch_size` parameter. Using the DontSendEntry table ============================= django-mailer creates a ``DontSendEntry`` model, which is used to filter out recipients from messages being created. But beware, it's actually only used when directly sending messages through mailer, not when mailer is used as an alternate ``EMAIL_BACKEND`` for Django. Also, even if recipients become empty due to this filtering, the email will be queued for sending anyway. (A patch to fix these issues would be accepted) django-mailer-2.0/manage.py000077500000000000000000000010711354206321500156710ustar00rootroot00000000000000#!/usr/bin/env python DEFAULT_SETTINGS = dict( INSTALLED_APPS=[ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sites", "mailer", ], DATABASES={ "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", } }, SITE_ID=1, SECRET_KEY="notasecret", ) if __name__ == "__main__": from django.conf import settings from django.core import management settings.configure(**DEFAULT_SETTINGS) management.execute_from_command_line() django-mailer-2.0/release.sh000077500000000000000000000005151354206321500160450ustar00rootroot00000000000000#!/bin/sh umask 000 rm -rf build dist git ls-tree --full-tree --name-only -r HEAD | xargs chmod ugo+r find src docs tests -type d | xargs chmod ugo+rx ./setup.py sdist bdist_wheel || exit 1 VERSION=$(./setup.py --version) || exit 1 twine upload dist/django_mailer-$VERSION-py2.py3-none-any.whl dist/django-mailer-$VERSION.tar.gz django-mailer-2.0/runtests.py000077500000000000000000000020041354206321500163250ustar00rootroot00000000000000#!/usr/bin/env python import os import sys import warnings import django from django.conf import settings from django.test.utils import get_runner warnings.simplefilter("always", DeprecationWarning) DEFAULT_SETTINGS = dict( INSTALLED_APPS=[ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sites", "mailer", ], DATABASES={ "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", } }, SITE_ID=1, SECRET_KEY="notasecret", MIDDLEWARE_CLASSES=[], ) def runtests(*test_args): if not settings.configured: settings.configure(**DEFAULT_SETTINGS) if not test_args: test_args = ['tests'] os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() failures = test_runner.run_tests(test_args) sys.exit(bool(failures)) if __name__ == "__main__": runtests(*sys.argv[1:]) django-mailer-2.0/setup.cfg000066400000000000000000000001661354206321500157110ustar00rootroot00000000000000[bdist_wheel] universal = 1 [flake8] max-line-length=100 max-complexity=10 exclude=src/mailer/migrations,build,.tox django-mailer-2.0/setup.py000077500000000000000000000022111354206321500155760ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup, find_packages setup( name="django-mailer", version="2.0", description="A reusable Django app for queuing the sending of email", long_description=open("docs/usage.rst").read() + open("CHANGES.rst").read(), author="Pinax Team", author_email="developers@pinaxproject.com", url="http://github.com/pinax/django-mailer/", packages=find_packages(where="src"), package_dir={"": "src"}, package_data={'mailer': ['locale/*/LC_MESSAGES/*.*']}, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Framework :: Django", ], install_requires=[ 'Django >= 1.11', 'lockfile >= 0.8', 'six', ], ) django-mailer-2.0/src/000077500000000000000000000000001354206321500146545ustar00rootroot00000000000000django-mailer-2.0/src/mailer/000077500000000000000000000000001354206321500161255ustar00rootroot00000000000000django-mailer-2.0/src/mailer/__init__.py000066400000000000000000000065551354206321500202510ustar00rootroot00000000000000from __future__ import absolute_import import warnings __version__ = '2.0' def get_priority(priority): from mailer.models import PRIORITY_MAPPING, PRIORITY_MEDIUM if priority is None: priority = PRIORITY_MEDIUM if priority in PRIORITY_MAPPING: warnings.warn("Please pass one of the PRIORITY_* constants to 'send_mail' " "and 'send_html_mail', not '{0}'.".format(priority), DeprecationWarning) priority = PRIORITY_MAPPING[priority] if priority not in PRIORITY_MAPPING.values(): raise ValueError("Invalid priority {0}".format(repr(priority))) return priority # replacement for django.core.mail.send_mail def send_mail(subject, message, from_email, recipient_list, priority=None, fail_silently=False, auth_user=None, auth_password=None): from django.utils.encoding import force_text from mailer.models import make_message priority = get_priority(priority) # need to do this in case subject used lazy version of ugettext subject = force_text(subject) message = force_text(message) make_message(subject=subject, body=message, from_email=from_email, to=recipient_list, priority=priority).save() return 1 def send_html_mail(subject, message, message_html, from_email, recipient_list, priority=None, fail_silently=False, auth_user=None, auth_password=None, headers={}): """ Function to queue HTML e-mails """ from django.utils.encoding import force_text from django.core.mail import EmailMultiAlternatives from mailer.models import make_message priority = get_priority(priority) # need to do this in case subject used lazy version of ugettext subject = force_text(subject) message = force_text(message) msg = make_message(subject=subject, body=message, from_email=from_email, to=recipient_list, priority=priority) email = msg.email email = EmailMultiAlternatives( email.subject, email.body, email.from_email, email.to, headers=headers ) email.attach_alternative(message_html, "text/html") msg.email = email msg.save() return 1 def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None): num_sent = 0 for subject, message, sender, recipient in datatuple: num_sent += send_mail(subject, message, sender, recipient) return num_sent def mail_admins(subject, message, fail_silently=False, connection=None, priority=None): from django.conf import settings from django.utils.encoding import force_text return send_mail(settings.EMAIL_SUBJECT_PREFIX + force_text(subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]) def mail_managers(subject, message, fail_silently=False, connection=None, priority=None): from django.conf import settings from django.utils.encoding import force_text return send_mail(settings.EMAIL_SUBJECT_PREFIX + force_text(subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]) django-mailer-2.0/src/mailer/admin.py000066400000000000000000000023221354206321500175660ustar00rootroot00000000000000from __future__ import unicode_literals from django.contrib import admin from mailer.models import Message, DontSendEntry, MessageLog def show_to(message): return ", ".join(message.to_addresses) show_to.short_description = "To" # noqa: E305 class MessageAdminMixin(object): def plain_text_body(self, instance): email = instance.email if hasattr(email, 'body'): return email.body else: return "" class MessageAdmin(MessageAdminMixin, admin.ModelAdmin): list_display = ["id", show_to, "subject", "when_added", "priority"] readonly_fields = ['plain_text_body'] date_hierarchy = "when_added" class DontSendEntryAdmin(admin.ModelAdmin): list_display = ["to_address", "when_added"] class MessageLogAdmin(MessageAdminMixin, admin.ModelAdmin): list_display = ["id", show_to, "subject", "message_id", "when_attempted", "result"] list_filter = ["result"] date_hierarchy = "when_attempted" readonly_fields = ['plain_text_body', 'message_id'] search_fields = ['message_id'] admin.site.register(Message, MessageAdmin) admin.site.register(DontSendEntry, DontSendEntryAdmin) admin.site.register(MessageLog, MessageLogAdmin) django-mailer-2.0/src/mailer/backend.py000066400000000000000000000010451354206321500200660ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings from django.core.mail.backends.base import BaseEmailBackend from mailer.models import Message class DbBackend(BaseEmailBackend): def send_messages(self, email_messages): # allow for a custom batch size MESSAGES_BATCH_SIZE = getattr(settings, "MAILER_MESSAGES_BATCH_SIZE", None) messages = Message.objects.bulk_create([ Message(email=email) for email in email_messages ], MESSAGES_BATCH_SIZE) return len(messages) django-mailer-2.0/src/mailer/engine.py000066400000000000000000000207231354206321500177500ustar00rootroot00000000000000from __future__ import unicode_literals import contextlib import logging import smtplib import time from socket import error as socket_error import lockfile from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.mail import get_connection from django.core.mail.message import make_msgid from django.db import NotSupportedError, OperationalError, transaction from mailer.models import (RESULT_FAILURE, RESULT_SUCCESS, Message, MessageLog, get_message_id) # when queue is empty, how long to wait (in seconds) before checking again EMPTY_QUEUE_SLEEP = getattr(settings, "MAILER_EMPTY_QUEUE_SLEEP", 30) # lock timeout value. how long to wait for the lock to become available. # default behavior is to never wait for the lock to be available. LOCK_WAIT_TIMEOUT = getattr(settings, "MAILER_LOCK_WAIT_TIMEOUT", -1) # allows for a different lockfile path. The default is a file # in the current working directory. LOCK_PATH = getattr(settings, "MAILER_LOCK_PATH", None) def prioritize(): """ Returns the messages in the queue in the order they should be sent. """ return Message.objects.non_deferred().order_by('priority', 'when_added') @contextlib.contextmanager def sender_context(message): """ Makes a context manager appropriate for sending a message. Entering the context using `with` may return a `None` object if the message has been sent/deleted already. """ # We wrap each message sending inside a transaction (otherwise # select_for_update doesn't work). # We also do `nowait` for databases that support it. The result of this is # that if two processes (which might be on different machines) both attempt # to send the same queue, the loser for the first message will immediately # get an error, and will be able to try the second message. This means the # work for sending the messages will be distributed between the two # processes. Otherwise, the losing process has to wait for the winning # process to finish and release the lock, and the winning process will # almost always win the next message etc. with transaction.atomic(): try: try: yield Message.objects.filter(id=message.id).select_for_update(nowait=True).get() except NotSupportedError: # MySQL yield Message.objects.filter(id=message.id).select_for_update().get() except Message.DoesNotExist: # Deleted by someone else yield None except OperationalError: # Locked by someone else yield None def get_messages_for_sending(): """ Returns a series of context managers that are used for sending mails in the queue. Entering the context manager returns the actual message """ for message in prioritize(): yield sender_context(message) def ensure_message_id(msg): if get_message_id(msg) is None: msg.extra_headers['Message-ID'] = make_msgid() def _limits_reached(sent, deferred): # Allow sending a fixed/limited amount of emails in each delivery run # defaults to None which means send everything in the queue EMAIL_MAX_BATCH = getattr(settings, "MAILER_EMAIL_MAX_BATCH", None) if EMAIL_MAX_BATCH is not None and sent >= EMAIL_MAX_BATCH: logging.info("EMAIL_MAX_BATCH (%s) reached, " "stopping for this round", EMAIL_MAX_BATCH) return True # Stop sending emails in the current round if more than X emails get # deferred - defaults to None which means keep going regardless EMAIL_MAX_DEFERRED = getattr(settings, "MAILER_EMAIL_MAX_DEFERRED", None) if EMAIL_MAX_DEFERRED is not None and deferred >= EMAIL_MAX_DEFERRED: logging.warning("EMAIL_MAX_DEFERRED (%s) reached, " "stopping for this round", EMAIL_MAX_DEFERRED) return True def _throttle_emails(): # When delivering, wait some time between emails to avoid server overload # defaults to 0 for no waiting EMAIL_THROTTLE = getattr(settings, "MAILER_EMAIL_THROTTLE", 0) if EMAIL_THROTTLE: logging.debug("Throttling email delivery. " "Sleeping %s seconds", EMAIL_THROTTLE) time.sleep(EMAIL_THROTTLE) def acquire_lock(): logging.debug("acquiring lock...") if LOCK_PATH is not None: lock_file_path = LOCK_PATH else: lock_file_path = "send_mail" lock = lockfile.FileLock(lock_file_path) try: lock.acquire(LOCK_WAIT_TIMEOUT) except lockfile.AlreadyLocked: logging.debug("lock already in place. quitting.") return False, lock except lockfile.LockTimeout: logging.debug("waiting for the lock timed out. quitting.") return False, lock logging.debug("acquired.") return True, lock def release_lock(lock): logging.debug("releasing lock...") lock.release() logging.debug("released.") def _require_no_backend_loop(mailer_email_backend): if mailer_email_backend == settings.EMAIL_BACKEND == 'mailer.backend.DbBackend': raise ImproperlyConfigured('EMAIL_BACKEND and MAILER_EMAIL_BACKEND' ' should not both be set to "{}"' ' at the same time' .format(settings.EMAIL_BACKEND)) def send_all(): """ Send all eligible messages in the queue. """ # The actual backend to use for sending, defaulting to the Django default. # To make testing easier this is not stored at module level. mailer_email_backend = getattr( settings, "MAILER_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend" ) _require_no_backend_loop(mailer_email_backend) acquired, lock = acquire_lock() if not acquired: return start_time = time.time() deferred = 0 sent = 0 try: connection = None for context in get_messages_for_sending(): with context as message: if message is None: # We didn't acquire the lock continue try: if connection is None: connection = get_connection(backend=mailer_email_backend) logging.info("sending message '{0}' to {1}".format( message.subject, ", ".join(message.to_addresses)) ) email = message.email if email is not None: email.connection = connection ensure_message_id(email) email.send() # connection can't be stored in the MessageLog email.connection = None message.email = email # For the sake of MessageLog MessageLog.objects.log(message, RESULT_SUCCESS) sent += 1 else: logging.warning("message discarded due to failure in converting from DB. Added on '%s' with priority '%s'" % (message.when_added, message.priority)) # noqa message.delete() except (socket_error, smtplib.SMTPSenderRefused, smtplib.SMTPRecipientsRefused, smtplib.SMTPDataError, smtplib.SMTPAuthenticationError) as err: message.defer() logging.info("message deferred due to failure: %s" % err) MessageLog.objects.log(message, RESULT_FAILURE, log_message=str(err)) deferred += 1 # Get new connection, it case the connection itself has an error. connection = None # Check if we reached the limits for the current run if _limits_reached(sent, deferred): break _throttle_emails() finally: release_lock(lock) logging.info("") logging.info("%s sent; %s deferred;" % (sent, deferred)) logging.info("done in %.2f seconds" % (time.time() - start_time)) def send_loop(): """ Loop indefinitely, checking queue at intervals of EMPTY_QUEUE_SLEEP and sending messages if any are on queue. """ while True: while not Message.objects.all(): logging.debug("sleeping for %s seconds before checking queue again" % EMPTY_QUEUE_SLEEP) time.sleep(EMPTY_QUEUE_SLEEP) send_all() django-mailer-2.0/src/mailer/locale/000077500000000000000000000000001354206321500173645ustar00rootroot00000000000000django-mailer-2.0/src/mailer/locale/ja/000077500000000000000000000000001354206321500177565ustar00rootroot00000000000000django-mailer-2.0/src/mailer/locale/ja/LC_MESSAGES/000077500000000000000000000000001354206321500215435ustar00rootroot00000000000000django-mailer-2.0/src/mailer/locale/ja/LC_MESSAGES/django.po000066400000000000000000000022141354206321500233440ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-02-10 13:24+0900\n" "PO-Revision-Date: 2013-07-08 12:35+0700\n" "Last-Translator: Basil Shubin \n" "Language-Team: \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 1.5.4\n" #: mailer/models.py:122 msgid "message" msgstr "メッセージ" #: mailer/models.py:123 msgid "messages" msgstr "メッセージ" #: mailer/models.py:234 msgid "don't send entry" msgstr "遅延メッセージ" #: mailer/models.py:235 msgid "don't send entries" msgstr "遅延メッセージ" #: mailer/models.py:293 msgid "message log" msgstr "メッセージログ" #: mailer/models.py:294 msgid "message logs" msgstr "メッセージログ" django-mailer-2.0/src/mailer/locale/ru/000077500000000000000000000000001354206321500200125ustar00rootroot00000000000000django-mailer-2.0/src/mailer/locale/ru/LC_MESSAGES/000077500000000000000000000000001354206321500215775ustar00rootroot00000000000000django-mailer-2.0/src/mailer/locale/ru/LC_MESSAGES/django.po000066400000000000000000000022231354206321500234000ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-07-08 12:32+0700\n" "PO-Revision-Date: 2013-07-08 12:35+0700\n" "Last-Translator: Basil Shubin \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 1.5.4\n" #: models.py:102 msgid "message" msgstr "сообщение" #: models.py:103 msgid "messages" msgstr "сообщения" #: models.py:203 msgid "don't send entry" msgstr "отложенное сообщение" #: models.py:204 msgid "don't send entries" msgstr "отложенные сообщения" #: models.py:249 msgid "message log" msgstr "журнальная запись" #: models.py:250 msgid "message logs" msgstr "журнал сообщений" django-mailer-2.0/src/mailer/management/000077500000000000000000000000001354206321500202415ustar00rootroot00000000000000django-mailer-2.0/src/mailer/management/__init__.py000066400000000000000000000000001354206321500223400ustar00rootroot00000000000000django-mailer-2.0/src/mailer/management/commands/000077500000000000000000000000001354206321500220425ustar00rootroot00000000000000django-mailer-2.0/src/mailer/management/commands/__init__.py000066400000000000000000000000001354206321500241410ustar00rootroot00000000000000django-mailer-2.0/src/mailer/management/commands/purge_mail_log.py000066400000000000000000000007661354206321500254120ustar00rootroot00000000000000import logging from django.core.management.base import BaseCommand from mailer.models import MessageLog class Command(BaseCommand): help = "Delete mailer log" def add_arguments(self, parser): parser.add_argument('days', nargs=1, type=int) def handle(self, *args, **options): # Compatiblity with Django-1.6 days = int(options.get('days', args)[0]) count = MessageLog.objects.purge_old_entries(days) logging.info("%s log entries deleted " % count) django-mailer-2.0/src/mailer/management/commands/retry_deferred.py000066400000000000000000000011631354206321500254220ustar00rootroot00000000000000import logging from django.core.management.base import BaseCommand from mailer.models import Message from mailer.management.helpers import CronArgMixin class Command(CronArgMixin, BaseCommand): help = "Attempt to resend any deferred mail." def handle(self, *args, **options): if options['cron'] == 0: logging.basicConfig(level=logging.DEBUG, format="%(message)s") else: logging.basicConfig(level=logging.ERROR, format="%(message)s") count = Message.objects.retry_deferred() # @@@ new_priority not yet supported logging.info("%s message(s) retried" % count) django-mailer-2.0/src/mailer/management/commands/send_mail.py000066400000000000000000000015561354206321500243560ustar00rootroot00000000000000import logging from django.conf import settings from django.core.management.base import BaseCommand from mailer.engine import send_all from mailer.management.helpers import CronArgMixin # allow a sysadmin to pause the sending of mail temporarily. PAUSE_SEND = getattr(settings, "MAILER_PAUSE_SEND", False) class Command(CronArgMixin, BaseCommand): help = "Do one pass through the mail queue, attempting to send all mail." def handle(self, *args, **options): if options['cron'] == 0: logging.basicConfig(level=logging.DEBUG, format="%(message)s") else: logging.basicConfig(level=logging.ERROR, format="%(message)s") logging.info("-" * 72) # if PAUSE_SEND is turned on don't do anything. if not PAUSE_SEND: send_all() else: logging.info("sending is paused, quitting.") django-mailer-2.0/src/mailer/management/helpers.py000066400000000000000000000013521354206321500222560ustar00rootroot00000000000000from optparse import make_option from django.core.management.base import BaseCommand help_msg = "If 1 don't print messagges, but only errors." if hasattr(BaseCommand, 'option_list') and BaseCommand.option_list: # pre django 1.8; use optparse class CronArgMixin(object): base_options = ( make_option('-c', '--cron', default=0, type='int', help=help_msg), ) option_list = BaseCommand.option_list + base_options else: # django 1.8+; use argparse class CronArgMixin(object): def add_arguments(self, parser): parser.add_argument( '-c', '--cron', default=0, type=int, help=help_msg, ) django-mailer-2.0/src/mailer/migrations/000077500000000000000000000000001354206321500203015ustar00rootroot00000000000000django-mailer-2.0/src/mailer/migrations/0001_initial.py000066400000000000000000000043611354206321500227500ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ] operations = [ migrations.CreateModel( name='DontSendEntry', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('to_address', models.EmailField(max_length=254)), ('when_added', models.DateTimeField()), ], options={ 'verbose_name': "don't send entry", 'verbose_name_plural': "don't send entries", }, ), migrations.CreateModel( name='Message', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('message_data', models.TextField()), ('when_added', models.DateTimeField(default=django.utils.timezone.now)), ('priority', models.CharField(default='2', max_length=1, choices=[('1', 'high'), ('2', 'medium'), ('3', 'low'), ('4', 'deferred')])), ], options={ 'verbose_name': 'message', 'verbose_name_plural': 'messages', }, ), migrations.CreateModel( name='MessageLog', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('message_data', models.TextField()), ('when_added', models.DateTimeField(db_index=True)), ('priority', models.CharField(db_index=True, max_length=1, choices=[('1', 'high'), ('2', 'medium'), ('3', 'low'), ('4', 'deferred')])), ('when_attempted', models.DateTimeField(default=django.utils.timezone.now)), ('result', models.CharField(max_length=1, choices=[('1', 'success'), ('2', "don't send"), ('3', 'failure')])), ('log_message', models.TextField()), ], options={ 'verbose_name': 'message log', 'verbose_name_plural': 'message logs', }, ), ] django-mailer-2.0/src/mailer/migrations/0002_auto_20150720_1433.py000066400000000000000000000012621354206321500237170ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations # This might be a no-op migration on some installations. However, some # installations will have had EmailField with a default max_length=75 (pre # Django 1.7), and we didn't account for this properly before, so this migration # fixes everything to be max_length=254 in case it wasn't. class Migration(migrations.Migration): dependencies = [ ('mailer', '0001_initial'), ] operations = [ migrations.AlterField( model_name='dontsendentry', name='to_address', field=models.EmailField(max_length=254), ), ] django-mailer-2.0/src/mailer/migrations/0003_messagelog_message_id.py000066400000000000000000000007231354206321500256250ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-04-16 23:26 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('mailer', '0002_auto_20150720_1433'), ] operations = [ migrations.AddField( model_name='messagelog', name='message_id', field=models.TextField(editable=False, null=True), ), ] django-mailer-2.0/src/mailer/migrations/0004_auto_20190920_1512.py000066400000000000000000000013101354206321500237170ustar00rootroot00000000000000# Generated by Django 3.0a1 on 2019-09-20 15:12 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('mailer', '0003_messagelog_message_id'), ] operations = [ migrations.AlterField( model_name='message', name='priority', field=models.PositiveSmallIntegerField(choices=[(1, 'high'), (2, 'medium'), (3, 'low'), (4, 'deferred')], default=2), ), migrations.AlterField( model_name='messagelog', name='priority', field=models.PositiveSmallIntegerField(choices=[(1, 'high'), (2, 'medium'), (3, 'low'), (4, 'deferred')], db_index=True), ), ] django-mailer-2.0/src/mailer/migrations/__init__.py000066400000000000000000000000001354206321500224000ustar00rootroot00000000000000django-mailer-2.0/src/mailer/models.py000066400000000000000000000210371354206321500177650ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import logging import pickle import datetime try: from django.utils.encoding import python_2_unicode_compatible except ImportError: def python_2_unicode_compatible(c): return c from django.utils.timezone import now as datetime_now from django.core.mail import EmailMessage from django.db import models from django.utils.translation import ugettext_lazy as _ PRIORITY_HIGH = 1 PRIORITY_MEDIUM = 2 PRIORITY_LOW = 3 PRIORITY_DEFERRED = 4 PRIORITIES = [ (PRIORITY_HIGH, "high"), (PRIORITY_MEDIUM, "medium"), (PRIORITY_LOW, "low"), (PRIORITY_DEFERRED, "deferred"), ] PRIORITY_MAPPING = dict((label, v) for (v, label) in PRIORITIES) def get_message_id(msg): # From django.core.mail.message: Email header names are case-insensitive # (RFC 2045), so we have to accommodate that when doing comparisons. for key, value in msg.extra_headers.items(): if key.lower() == 'message-id': return value class MessageManager(models.Manager): def high_priority(self): """ the high priority messages in the queue """ return self.filter(priority=PRIORITY_HIGH) def medium_priority(self): """ the medium priority messages in the queue """ return self.filter(priority=PRIORITY_MEDIUM) def low_priority(self): """ the low priority messages in the queue """ return self.filter(priority=PRIORITY_LOW) def non_deferred(self): """ the messages in the queue not deferred """ return self.exclude(priority=PRIORITY_DEFERRED) def deferred(self): """ the deferred messages in the queue """ return self.filter(priority=PRIORITY_DEFERRED) def retry_deferred(self, new_priority=PRIORITY_MEDIUM): return self.deferred().update(priority=new_priority) base64_encode = base64.encodebytes if hasattr(base64, 'encodebytes') else base64.encodestring base64_decode = base64.decodebytes if hasattr(base64, 'decodebytes') else base64.decodestring def email_to_db(email): # pickle.dumps returns essentially binary data which we need to base64 # encode to store in a unicode field. finally we encode back to make sure # we only try to insert unicode strings into the db, since we use a # TextField return base64_encode(pickle.dumps(email)).decode('ascii') def db_to_email(data): if data == "": return None else: try: data = data.encode("ascii") except AttributeError: pass try: return pickle.loads(base64_decode(data)) except (TypeError, pickle.UnpicklingError, base64.binascii.Error, AttributeError): try: # previous method was to just do pickle.dumps(val) return pickle.loads(data) except (TypeError, pickle.UnpicklingError, AttributeError): return None @python_2_unicode_compatible class Message(models.Model): # The actual data - a pickled EmailMessage message_data = models.TextField() when_added = models.DateTimeField(default=datetime_now) priority = models.PositiveSmallIntegerField(choices=PRIORITIES, default=PRIORITY_MEDIUM) objects = MessageManager() class Meta: verbose_name = _("message") verbose_name_plural = _("messages") def __str__(self): try: email = self.email return "On {0}, \"{1}\" to {2}".format(self.when_added, email.subject, ", ".join(email.to)) except Exception: return "" def defer(self): self.priority = PRIORITY_DEFERRED self.save() def _get_email(self): return db_to_email(self.message_data) def _set_email(self, val): self.message_data = email_to_db(val) email = property( _get_email, _set_email, doc="""EmailMessage object. If this is mutated, you will need to set the attribute again to cause the underlying serialised data to be updated.""") @property def to_addresses(self): email = self.email if email is not None: return email.to else: return [] @property def subject(self): email = self.email if email is not None: return email.subject else: return "" def filter_recipient_list(lst): if lst is None: return None retval = [] for e in lst: if DontSendEntry.objects.has_address(e): logging.info("skipping email to %s as on don't send list " % e.encode("utf-8")) else: retval.append(e) return retval def make_message(subject="", body="", from_email=None, to=None, bcc=None, attachments=None, headers=None, priority=None): """ Creates a simple message for the email parameters supplied. The 'to' and 'bcc' lists are filtered using DontSendEntry. If needed, the 'email' attribute can be set to any instance of EmailMessage if e-mails with attachments etc. need to be supported. Call 'save()' on the result when it is ready to be sent, and not before. """ to = filter_recipient_list(to) bcc = filter_recipient_list(bcc) core_msg = EmailMessage( subject=subject, body=body, from_email=from_email, to=to, bcc=bcc, attachments=attachments, headers=headers ) db_msg = Message(priority=priority) db_msg.email = core_msg return db_msg class DontSendEntryManager(models.Manager): def has_address(self, address): """ is the given address on the don't send list? """ queryset = self.filter(to_address__iexact=address) return queryset.exists() class DontSendEntry(models.Model): to_address = models.EmailField(max_length=254) when_added = models.DateTimeField() objects = DontSendEntryManager() class Meta: verbose_name = _("don't send entry") verbose_name_plural = _("don't send entries") RESULT_SUCCESS = "1" RESULT_DONT_SEND = "2" RESULT_FAILURE = "3" RESULT_CODES = ( (RESULT_SUCCESS, "success"), (RESULT_DONT_SEND, "don't send"), (RESULT_FAILURE, "failure"), # @@@ other types of failure? ) class MessageLogManager(models.Manager): def log(self, message, result_code, log_message=""): """ create a log entry for an attempt to send the given message and record the given result and (optionally) a log message """ return self.create( message_data=message.message_data, message_id=get_message_id(message.email), when_added=message.when_added, priority=message.priority, result=result_code, log_message=log_message, ) def purge_old_entries(self, days): limit = datetime_now() - datetime.timedelta(days=days) query = self.filter(when_attempted__lt=limit, result=RESULT_SUCCESS) count = query.count() query.delete() return count @python_2_unicode_compatible class MessageLog(models.Model): # fields from Message message_data = models.TextField() message_id = models.TextField(editable=False, null=True) when_added = models.DateTimeField(db_index=True) priority = models.PositiveSmallIntegerField(choices=PRIORITIES, db_index=True) # additional logging fields when_attempted = models.DateTimeField(default=datetime_now) result = models.CharField(max_length=1, choices=RESULT_CODES) log_message = models.TextField() objects = MessageLogManager() class Meta: verbose_name = _("message log") verbose_name_plural = _("message logs") def __str__(self): try: email = self.email return "On {0}, \"{1}\" to {2}".format(self.when_attempted, email.subject, ", ".join(email.to)) except Exception: return "" @property def email(self): return db_to_email(self.message_data) @property def to_addresses(self): email = self.email if email is not None: return email.to else: return [] @property def subject(self): email = self.email if email is not None: return email.subject else: return "" django-mailer-2.0/tests/000077500000000000000000000000001354206321500152275ustar00rootroot00000000000000django-mailer-2.0/tests/__init__.py000066400000000000000000000733331354206321500173510ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime import pickle import smtplib import time import django import lockfile from django.core import mail from django.core.exceptions import ImproperlyConfigured from django.core.mail.backends.locmem import EmailBackend as LocMemEmailBackend from django.core.management import call_command from django.test import TestCase from django.utils.timezone import now as datetime_now from mock import ANY, Mock, patch import six import mailer from mailer import engine from mailer.models import (PRIORITY_DEFERRED, PRIORITY_HIGH, PRIORITY_LOW, PRIORITY_MEDIUM, RESULT_FAILURE, RESULT_SUCCESS, DontSendEntry, Message, MessageLog, db_to_email, email_to_db, make_message) class FakeConnection(object): def __getstate__(self): raise TypeError("Connections can't be pickled") class TestMailerEmailBackend(object): outbox = [] def __init__(self, **kwargs): self.connection = FakeConnection() del self.outbox[:] def open(self): pass def close(self): pass def send_messages(self, email_messages): for m in email_messages: m.extra_headers['X-Sent-By'] = 'django-mailer-tests' self.outbox.extend(email_messages) class FailingMailerEmailBackend(LocMemEmailBackend): def send_messages(self, email_messages): raise smtplib.SMTPSenderRefused(1, "foo", "foo@foo.com") class BackendTest(TestCase): def test_save_to_db(self): """ Test that using send_mail creates a Message object in DB instead, when EMAIL_BACKEND is set. """ self.assertEqual(Message.objects.count(), 0) with self.settings(EMAIL_BACKEND="mailer.backend.DbBackend"): mail.send_mail("Subject ☺", "Body", "sender@example.com", ["recipient@example.com"]) self.assertEqual(Message.objects.count(), 1) def test_save_mass_mail_to_db(self): """ Test that using send_mass_mail creates multiple Message objects in DB instead, when EMAIL_BACKEND is set. """ self.assertEqual(Message.objects.count(), 0) with self.settings(EMAIL_BACKEND="mailer.backend.DbBackend"): message1 = ('Subject ☺', 'Body', 'sender@example.com', ['first@example.com']) message2 = ('Another Subject ☺', 'Body', 'sender@example.com', ['second@test.com']) mail.send_mass_mail((message1, message2)) self.assertEqual(Message.objects.count(), 2) class SendingTest(TestCase): def setUp(self): # Ensure outbox is empty at start del TestMailerEmailBackend.outbox[:] def test_mailer_email_backend(self): """ Test that calling "manage.py send_mail" actually sends mail using the specified MAILER_EMAIL_BACKEND """ with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend"): mailer.send_mail("Subject ☺", "Body", "sender1@example.com", ["recipient@example.com"]) self.assertEqual(Message.objects.count(), 1) self.assertEqual(len(TestMailerEmailBackend.outbox), 0) engine.send_all() self.assertEqual(len(TestMailerEmailBackend.outbox), 1) self.assertEqual(Message.objects.count(), 0) self.assertEqual(MessageLog.objects.count(), 1) def test_retry_deferred(self): with self.settings(MAILER_EMAIL_BACKEND="tests.FailingMailerEmailBackend"): mailer.send_mail("Subject", "Body", "sender2@example.com", ["recipient@example.com"]) engine.send_all() self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 1) self.assertEqual(MessageLog.objects.count(), 1) with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): engine.send_all() self.assertEqual(len(mail.outbox), 0) # Should not have sent the deferred ones self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 1) # Now mark them for retrying Message.objects.retry_deferred() engine.send_all() self.assertEqual(len(mail.outbox), 1) self.assertEqual(Message.objects.count(), 0) def test_purge_old_entries(self): # Send one successfully with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_mail("Subject", "Body", "sender1@example.com", ["recipient@example.com"]) engine.send_all() # And one failure with self.settings(MAILER_EMAIL_BACKEND="tests.FailingMailerEmailBackend"): mailer.send_mail("Subject", "Body", "sender2@example.com", ["recipient@example.com"]) engine.send_all() Message.objects.retry_deferred() engine.send_all() with patch.object(mailer.models, 'datetime_now') as datetime_now_patch: datetime_now_patch.return_value = datetime_now() + datetime.timedelta(days=2) call_command('purge_mail_log', '1') self.assertNotEqual(MessageLog.objects.filter(result=RESULT_FAILURE).count(), 0) self.assertEqual(MessageLog.objects.filter(result=RESULT_SUCCESS).count(), 0) def test_send_loop(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): with patch("mailer.engine.send_all", side_effect=StopIteration) as send: with patch("time.sleep", side_effect=StopIteration) as sleep: self.assertRaises(StopIteration, engine.send_loop) sleep.assert_called_once_with(engine.EMPTY_QUEUE_SLEEP) send.assert_not_called() mailer.send_mail("Subject", "Body", "sender15@example.com", ["rec@example.com"]) self.assertRaises(StopIteration, engine.send_loop) send.assert_called_once() def test_send_html(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_html_mail("Subject", "Body", "Body", "htmlsender1@example.com", ["recipient@example.com"], priority=PRIORITY_HIGH) # Ensure deferred was not deleted self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 0) engine.send_all() self.assertEqual(len(mail.outbox), 1) sent = mail.outbox[0] # Default "plain text" self.assertEqual(sent.body, "Body") self.assertEqual(sent.content_subtype, "plain") # Alternative "text/html" self.assertEqual(sent.alternatives[0], ("Body", "text/html")) def test_send_mass_mail(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mails = ( ("Subject ☺", "Body", "mass0@example.com", ["recipient0@example.com"]), ("Subject ☺", "Body", "mass1@example.com", ["recipient1@example.com"]), ("Subject ☺", "Body", "mass2@example.com", ["recipient2@example.com"]), ("Subject ☺", "Body", "mass3@example.com", ["recipient3@example.com"]), ) mailer.send_mass_mail(mails) self.assertEqual(Message.objects.count(), 4) self.assertEqual(Message.objects.deferred().count(), 0) engine.send_all() self.assertEqual(Message.objects.count(), 0) self.assertEqual(Message.objects.deferred().count(), 0) self.assertEqual(len(mail.outbox), 4) for i, sent in enumerate(mail.outbox): # Default "plain text" self.assertEqual(sent.subject, "Subject ☺") self.assertEqual(sent.from_email, "mass{0}@example.com".format(i)) self.assertEqual(sent.to, ["recipient{0}@example.com".format(i)]) def test_mail_admins(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", ADMINS=(("Test", "testadmin@example.com"),)): # noqa mailer.mail_admins("Subject", "Admin Body") self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 0) engine.send_all() self.assertEqual(Message.objects.count(), 0) self.assertEqual(Message.objects.deferred().count(), 0) self.assertEqual(len(mail.outbox), 1) sent = mail.outbox[0] # Default "plain text" self.assertEqual(sent.body, "Admin Body") self.assertEqual(sent.to, ["testadmin@example.com"]) def test_mail_managers(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", MANAGERS=(("Test", "testmanager@example.com"),)): # noqa mailer.mail_managers("Subject", "Manager Body") self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 0) engine.send_all() self.assertEqual(Message.objects.count(), 0) self.assertEqual(Message.objects.deferred().count(), 0) self.assertEqual(len(mail.outbox), 1) sent = mail.outbox[0] # Default "plain text" self.assertEqual(sent.body, "Manager Body") self.assertEqual(sent.to, ["testmanager@example.com"]) def test_blacklisted_emails(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): now = datetime_now() obj = DontSendEntry.objects.create(to_address="nogo@example.com", when_added=now) self.assertTrue(obj.to_address, "nogo@example.com") mailer.send_mail("Subject", "GoBody", "send1@example.com", ["go@example.com"]) mailer.send_mail("Subject", "NoGoBody", "send2@example.com", ["nogo@example.com"]) self.assertEqual(Message.objects.count(), 2) self.assertEqual(Message.objects.deferred().count(), 0) engine.send_all() # All messages are processed self.assertEqual(Message.objects.count(), 0) self.assertEqual(Message.objects.deferred().count(), 0) # but only one should get sent self.assertEqual(len(mail.outbox), 1) sent = mail.outbox[0] # Default "plain text" self.assertEqual(sent.body, "GoBody") self.assertEqual(sent.to, ["go@example.com"]) def test_control_max_delivery_amount(self): with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend", MAILER_EMAIL_MAX_BATCH=2): # noqa mailer.send_mail("Subject1", "Body1", "sender1@example.com", ["recipient1@example.com"]) mailer.send_mail("Subject2", "Body2", "sender2@example.com", ["recipient2@example.com"]) mailer.send_mail("Subject3", "Body3", "sender3@example.com", ["recipient3@example.com"]) self.assertEqual(Message.objects.count(), 3) self.assertEqual(len(TestMailerEmailBackend.outbox), 0) engine.send_all() self.assertEqual(len(TestMailerEmailBackend.outbox), 2) self.assertEqual(Message.objects.count(), 1) self.assertEqual(MessageLog.objects.count(), 2) def test_control_max_retry_amount(self): with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend"): # noqa # 5 normal emails scheduled for delivery mailer.send_mail("Subject1", "Body1", "sender1@example.com", ["recipient1@example.com"]) mailer.send_mail("Subject2", "Body2", "sender2@example.com", ["recipient2@example.com"]) mailer.send_mail("Subject3", "Body3", "sender3@example.com", ["recipient3@example.com"]) mailer.send_mail("Subject4", "Body4", "sender4@example.com", ["recipient4@example.com"]) mailer.send_mail("Subject5", "Body5", "sender5@example.com", ["recipient5@example.com"]) self.assertEqual(Message.objects.count(), 5) self.assertEqual(Message.objects.deferred().count(), 0) with self.settings(MAILER_EMAIL_BACKEND="tests.FailingMailerEmailBackend", MAILER_EMAIL_MAX_DEFERRED=2): # noqa # 2 will get deferred 3 remain undeferred with patch("logging.warning") as w: engine.send_all() w.assert_called_once() arg = w.call_args[0][0] self.assertIn("EMAIL_MAX_DEFERRED", arg) self.assertIn("stopping for this round", arg) self.assertEqual(Message.objects.count(), 5) self.assertEqual(Message.objects.deferred().count(), 2) with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend", MAILER_EMAIL_MAX_DEFERRED=2): # noqa # 3 will be delivered, 2 remain deferred engine.send_all() self.assertEqual(len(TestMailerEmailBackend.outbox), 3) # Should not have sent the deferred ones self.assertEqual(Message.objects.count(), 2) self.assertEqual(Message.objects.deferred().count(), 2) # Now mark them for retrying Message.objects.retry_deferred() engine.send_all() self.assertEqual(len(TestMailerEmailBackend.outbox), 2) self.assertEqual(Message.objects.count(), 0) def test_throttling_delivery(self): TIME = 1 # throttle time = 1 second with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend", MAILER_EMAIL_THROTTLE=TIME): # noqa mailer.send_mail("Subject", "Body", "sender13@example.com", ["recipient@example.com"]) mailer.send_mail("Subject", "Body", "sender14@example.com", ["recipient@example.com"]) start_time = time.time() engine.send_all() throttled_time = time.time() - start_time self.assertEqual(len(TestMailerEmailBackend.outbox), 2) self.assertEqual(Message.objects.count(), 0) # Notes: 2 * TIME because 2 emails are sent during the test self.assertGreater(throttled_time, 2 * TIME) def test_save_changes_to_email(self): """ Test that changes made to the email by the backend are saved in MessageLog. """ with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend"): mailer.send_mail("Subject", "Body", "sender@example.com", ["recipient@example.com"]) engine.send_all() m = MessageLog.objects.get() self.assertEqual(m.email.extra_headers['X-Sent-By'], 'django-mailer-tests') def test_set_and_save_message_id(self): """ Test that message-id is set and saved correctly """ with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend"): mailer.send_mail("Subject", "Body", "sender@example.com", ["recipient@example.com"]) engine.send_all() m = MessageLog.objects.get() self.assertEqual( m.email.extra_headers['Message-ID'], m.message_id ) def test_save_existing_message_id(self): """ Test that a preset message-id is saved correctly """ with self.settings(MAILER_EMAIL_BACKEND="tests.TestMailerEmailBackend"): make_message( subject="Subject", body="Body", from_email="sender@example.com", to=["recipient@example.com"], priority=PRIORITY_MEDIUM, headers={'message-id': 'foo'}, ).save() engine.send_all() m = MessageLog.objects.get() self.assertEqual( m.email.extra_headers['message-id'], 'foo' ) self.assertEqual( m.message_id, 'foo' ) class LockNormalTest(TestCase): def setUp(self): class CustomError(Exception): pass self.CustomError = CustomError self.lock_mock = Mock() self.patcher_lock = patch("lockfile.FileLock", return_value=self.lock_mock) self.patcher_prio = patch("mailer.engine.prioritize", side_effect=CustomError) self.lock = self.patcher_lock.start() self.prio = self.patcher_prio.start() def test(self): self.assertRaises(self.CustomError, engine.send_all) self.lock_mock.acquire.assert_called_once_with(engine.LOCK_WAIT_TIMEOUT) self.lock.assert_called_once_with("send_mail") self.prio.assert_called_once() def tearDown(self): self.patcher_lock.stop() self.patcher_prio.stop() class LockLockedTest(TestCase): def setUp(self): config = { "acquire.side_effect": lockfile.AlreadyLocked, } self.lock_mock = Mock(**config) self.patcher_lock = patch("lockfile.FileLock", return_value=self.lock_mock) self.patcher_prio = patch("mailer.engine.prioritize", side_effect=Exception) self.lock = self.patcher_lock.start() self.prio = self.patcher_prio.start() def test(self): engine.send_all() self.lock_mock.acquire.assert_called_once_with(engine.LOCK_WAIT_TIMEOUT) self.lock.assert_called_once_with("send_mail") self.prio.assert_not_called() def tearDown(self): self.patcher_lock.stop() self.patcher_prio.stop() class LockTimeoutTest(TestCase): def setUp(self): config = { "acquire.side_effect": lockfile.LockTimeout, } self.lock_mock = Mock(**config) self.patcher_lock = patch("lockfile.FileLock", return_value=self.lock_mock) self.patcher_prio = patch("mailer.engine.prioritize", side_effect=Exception) self.lock = self.patcher_lock.start() self.prio = self.patcher_prio.start() def test(self): engine.send_all() self.lock_mock.acquire.assert_called_once_with(engine.LOCK_WAIT_TIMEOUT) self.lock.assert_called_once_with("send_mail") self.prio.assert_not_called() def tearDown(self): self.patcher_lock.stop() self.patcher_prio.stop() class PrioritizeTest(TestCase): def test_prioritize(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_mail("Subject", "Body", "prio1@example.com", ["r@example.com"], priority=PRIORITY_HIGH) mailer.send_mail("Subject", "Body", "prio2@example.com", ["r@example.com"], priority=PRIORITY_MEDIUM) mailer.send_mail("Subject", "Body", "prio3@example.com", ["r@example.com"], priority=PRIORITY_LOW) mailer.send_mail("Subject", "Body", "prio4@example.com", ["r@example.com"], priority=PRIORITY_HIGH) mailer.send_mail("Subject", "Body", "prio5@example.com", ["r@example.com"], priority=PRIORITY_HIGH) mailer.send_mail("Subject", "Body", "prio6@example.com", ["r@example.com"], priority=PRIORITY_LOW) mailer.send_mail("Subject", "Body", "prio7@example.com", ["r@example.com"], priority=PRIORITY_LOW) mailer.send_mail("Subject", "Body", "prio8@example.com", ["r@example.com"], priority=PRIORITY_MEDIUM) mailer.send_mail("Subject", "Body", "prio9@example.com", ["r@example.com"], priority=PRIORITY_MEDIUM) mailer.send_mail("Subject", "Body", "prio10@example.com", ["r@example.com"], priority=PRIORITY_LOW) mailer.send_mail("Subject", "Body", "prio11@example.com", ["r@example.com"], priority=PRIORITY_MEDIUM) mailer.send_mail("Subject", "Body", "prio12@example.com", ["r@example.com"], priority=PRIORITY_HIGH) mailer.send_mail("Subject", "Body", "prio13@example.com", ["r@example.com"], priority=PRIORITY_DEFERRED) self.assertEqual(Message.objects.count(), 13) self.assertEqual(Message.objects.deferred().count(), 1) self.assertEqual(Message.objects.non_deferred().count(), 12) messages = iter(engine.prioritize()) # High priority msg = next(messages) self.assertEqual(msg.email.from_email, "prio1@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio4@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio5@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio12@example.com") msg.delete() # Medium priority msg = next(messages) self.assertEqual(msg.email.from_email, "prio2@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio8@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio9@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio11@example.com") msg.delete() # Low priority msg = next(messages) self.assertEqual(msg.email.from_email, "prio3@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio6@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio7@example.com") msg.delete() msg = next(messages) self.assertEqual(msg.email.from_email, "prio10@example.com") msg.delete() # Ensure nothing else comes up self.assertRaises(StopIteration, lambda: next(messages)) # Ensure deferred was not deleted self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 1) class MessagesTest(TestCase): def test_message(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_mail("Subject Msg", "Body", "msg1@example.com", ["rec1@example.com"]) self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 0) self.assertEqual(MessageLog.objects.count(), 0) msg = Message.objects.all()[0] self.assertEqual(msg.email.from_email, "msg1@example.com") self.assertEqual(msg.to_addresses, ["rec1@example.com"]) self.assertEqual(msg.subject, "Subject Msg") # Fake a msg stored in DB with invalid data msg.message_data = "" self.assertEqual(msg.to_addresses, []) self.assertEqual(msg.subject, "") msg.save() with patch("logging.warning") as w: engine.send_all() w.assert_called_once() arg = w.call_args[0][0] self.assertIn("message discarded due to failure in converting from DB", arg) self.assertEqual(Message.objects.count(), 0) self.assertEqual(Message.objects.deferred().count(), 0) # Delivery should discard broken messages self.assertEqual(MessageLog.objects.count(), 0) def test_message_log(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_mail("Subject Log", "Body", "log1@example.com", ["1gol@example.com"]) self.assertEqual(Message.objects.count(), 1) self.assertEqual(Message.objects.deferred().count(), 0) self.assertEqual(MessageLog.objects.count(), 0) engine.send_all() self.assertEqual(Message.objects.count(), 0) self.assertEqual(Message.objects.deferred().count(), 0) self.assertEqual(MessageLog.objects.count(), 1) log = MessageLog.objects.all()[0] self.assertEqual(log.email.from_email, "log1@example.com") self.assertEqual(log.to_addresses, ["1gol@example.com"]) self.assertEqual(log.subject, "Subject Log") # Fake a log entry without email log.message_data = "" self.assertEqual(log.to_addresses, []) self.assertEqual(log.subject, "") def test_message_str(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_mail("Subject Msg 中", "Body 中", "msg1@example.com", ["rec1@example.com"]) self.assertEqual(Message.objects.count(), 1) msg = Message.objects.get() self.assertEqual( six.text_type(msg), 'On {0}, "Subject Msg 中" to rec1@example.com'.format(msg.when_added), ) msg.message_data = None self.assertEqual(str(msg), '') def test_message_log_str(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): mailer.send_mail("Subject Log 中", "Body 中", "log1@example.com", ["1gol@example.com"]) engine.send_all() self.assertEqual(MessageLog.objects.count(), 1) log = MessageLog.objects.get() self.assertEqual( six.text_type(log), 'On {0}, "Subject Log 中" to 1gol@example.com'.format(log.when_attempted), ) log.message_data = None self.assertEqual(str(log), '') class DbToEmailTest(TestCase): def test_db_to_email(self): # Empty/Invalid content self.assertEqual(db_to_email(""), None) self.assertEqual(db_to_email(None), None) # Other objects which should be returned as-is data = "Hello Email" self.assertEqual(db_to_email(email_to_db(data)), data) data = ["Test subject", "Test body", "testsender@example.com", ["testrec@example.com"]] self.assertEqual(db_to_email(email_to_db(data)), data) email = mail.EmailMessage(*data) converted_email = db_to_email(email_to_db(email)) self.assertEqual(converted_email.body, email.body) self.assertEqual(converted_email.subject, email.subject) self.assertEqual(converted_email.from_email, email.from_email) self.assertEqual(converted_email.to, email.to) # Test old pickle in DB format db_email = pickle.dumps(email) converted_email = db_to_email(db_email) self.assertEqual(converted_email.body, email.body) self.assertEqual(converted_email.subject, email.subject) self.assertEqual(converted_email.from_email, email.from_email) self.assertEqual(converted_email.to, email.to) def call_command_with_cron_arg(command, cron_value): # for old django versions, `call_command` doesn't parse arguments if django.VERSION < (1, 8): return call_command(command, cron=cron_value) # newer django; test parsing by passing argument as string return call_command(command, '--cron={}'.format(cron_value)) class CommandHelperTest(TestCase): def test_send_mail_no_cron(self): with patch('mailer.management.commands.send_mail.logging') as logging: call_command('send_mail') logging.basicConfig.assert_called_with(level=logging.DEBUG, format=ANY) def test_send_mail_cron_0(self): with patch('mailer.management.commands.send_mail.logging') as logging: call_command_with_cron_arg('send_mail', 0) logging.basicConfig.assert_called_with(level=logging.DEBUG, format=ANY) def test_send_mail_cron_1(self): with patch('mailer.management.commands.send_mail.logging') as logging: call_command_with_cron_arg('send_mail', 1) logging.basicConfig.assert_called_with(level=logging.ERROR, format=ANY) def test_retry_deferred_no_cron(self): with patch('mailer.management.commands.retry_deferred.logging') as logging: call_command('retry_deferred') logging.basicConfig.assert_called_with(level=logging.DEBUG, format=ANY) def test_retry_deferred_cron_0(self): with patch('mailer.management.commands.retry_deferred.logging') as logging: call_command_with_cron_arg('retry_deferred', 0) logging.basicConfig.assert_called_with(level=logging.DEBUG, format=ANY) def test_retry_deferred_cron_1(self): with patch('mailer.management.commands.retry_deferred.logging') as logging: call_command_with_cron_arg('retry_deferred', 1) logging.basicConfig.assert_called_with(level=logging.ERROR, format=ANY) class EmailBackendSettingLoopTest(TestCase): def test_loop_detection(self): with self.settings(EMAIL_BACKEND='mailer.backend.DbBackend', MAILER_EMAIL_BACKEND='mailer.backend.DbBackend'), \ self.assertRaises(ImproperlyConfigured) as catcher: engine.send_all() self.assertIn('mailer.backend.DbBackend', str(catcher.exception)) self.assertIn('EMAIL_BACKEND', str(catcher.exception)) self.assertIn('MAILER_EMAIL_BACKEND', str(catcher.exception)) django-mailer-2.0/tox.ini000066400000000000000000000014611354206321500154020ustar00rootroot00000000000000[tox] # Remember to add to .travis.yml if this is added to. envlist = py27-django111-test, py34-django{111,20}-test, {py35,py36}-django{111,20}-test, py36-django{21,22,30}-test, {py27,py34,py35,py36}-flake, checkmanifest, [testenv] basepython = py27: python2.7 py34: python3.4 py35: python3.5 py36: python3.6 commands = test: coverage run ./runtests.py flake: flake8 --statistics --benchmark deps = six lockfile==0.10.2 coverage mock==3.0.5 django111: Django==1.11.20 django20: Django==2.0.13 django21: Django==2.1.7 django22: Django==2.2 django30: Django==3.0a1 flake: flake8==3.7.7 py27-flake: Django<2.0 [testenv:checkmanifest] basepython = python2.7 deps = check-manifest Django<3.0 commands = check-manifest