pax_global_header00006660000000000000000000000064142127006070014511gustar00rootroot0000000000000052 comment=70a476f32f934473e029d81dae1a612a14c153c2 django-mailer-2.2/000077500000000000000000000000001421270060700140655ustar00rootroot00000000000000django-mailer-2.2/.coveragerc000066400000000000000000000000701421270060700162030ustar00rootroot00000000000000[run] source = mailer omit = mailer/tests.py branch = 1 django-mailer-2.2/.gitattributes000066400000000000000000000000271421270060700167570ustar00rootroot00000000000000CHANGES.rst merge=uniondjango-mailer-2.2/.github/000077500000000000000000000000001421270060700154255ustar00rootroot00000000000000django-mailer-2.2/.github/workflows/000077500000000000000000000000001421270060700174625ustar00rootroot00000000000000django-mailer-2.2/.github/workflows/build.yml000066400000000000000000000051521421270060700213070ustar00rootroot00000000000000name: build on: push: branches: [ master ] pull_request: branches: [ master ] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Sync with tox.ini include: - python-version: 2.7 django-version: 1.11.29 - python-version: 3.5 django-version: 2.0.13 - python-version: 3.6 django-version: 2.0.13 - python-version: 3.7 django-version: 2.1.15 - python-version: 3.8 django-version: 2.2.19 - python-version: 3.9 django-version: 3.0.14 - python-version: 3.9 django-version: 3.1.12 - python-version: 3.9 django-version: 3.2.4 - python-version: 3.9 django-version: 4.0.3 - python-version: "3.10" django-version: 4.0.3 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v2 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements*.txt') }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install Django==${{ matrix.django-version }} pip install -e . pip install -r requirements-test.txt - name: Test suite run: | ./runtests.py flake8: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.9' - uses: actions/cache@v2 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ '3.9' }}-${{ hashFiles('requirements*.txt') }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e . pip install -r requirements-dev.txt - name: Lint with flake8 run: | flake8 --version flake8 . check-manifest: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.9' - uses: actions/cache@v2 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ '3.9' }}-${{ hashFiles('requirements*.txt') }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e . pip install -r requirements-dev.txt - name: check-manifest run: | check-manifest django-mailer-2.2/.gitignore000066400000000000000000000001261421270060700160540ustar00rootroot00000000000000dist/ build/ django_mailer.egg-info/ htmlcov/ .tox/ MANIFEST .coverage *.mo *~ *.pyc django-mailer-2.2/AUTHORS000066400000000000000000000003761421270060700151430ustar00rootroot00000000000000 The PRIMARY AUTHORS are: * James Tauber * Brian Rosner ADDITIONAL CONTRIBUTORS include: * Michael Trier * Doug Napoleone * Jannis Leidel * Luke Plant * Renato Alves * Paul Brown * Sebastian Pipping * Jaap Roes django-mailer-2.2/CHANGES.rst000066400000000000000000000111441421270060700156700ustar00rootroot00000000000000Change log ========== 2.2 - 2022-03-11 ---------------- * Migrate models ``id`` fields to ``BigAutoField``. * Add ``runmailer`` management command. This command starts a loop that * Added ``runmailer`` management command. This command starts a loop that frequently checks the database for new emails. The wait time between checks can be controlled using the ``MAILER_EMPTY_QUEUE_SLEEP`` setting. 2.1 - 2020-12-05 ---------------- * The ``retry_deferred`` and ``send_mail`` commands rely on the log level set in your django project now. The ``-c/--cron`` option in those commands has been deprecated and the logic to configure log levels and the message format has been removed. * Changed logging to use module specific loggers to avoid interfering with other loggers. * Added ``MAILER_USE_FILE_LOCK`` setting to allow disabling file based locking. * Added ``-r`` option to ``purge_mail_log`` management command. Thanks julienc91 * Fixed deprecation warnings on Django 3.1 * Use cached DNS_NAME for performance * Added ability to override the default error handler via the ``MAILER_ERROR_HANDLER`` settings key 2.0.1 - 2020-03-01 ------------------ * Fixed issue with migration that some people experienced (see `PR 118 `_) 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.2/CONTRIBUTING.rst000066400000000000000000000052731421270060700165350ustar00rootroot00000000000000How 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 separate 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.2/LICENSE000066400000000000000000000020661421270060700150760ustar00rootroot00000000000000Copyright (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.2/MANIFEST.in000066400000000000000000000003501421270060700156210ustar00rootroot00000000000000include AUTHORS include LICENSE include *.rst include MANIFEST.in include .coveragerc include tox.ini include *.py include *.sh include *.txt graft docs graft src graft tests global-exclude *.py[cod] __pycache__/* *.so *.dylib *~ django-mailer-2.2/README.rst000066400000000000000000000137751421270060700155710ustar00rootroot00000000000000Django Mailer ------------- .. image:: http://slack.pinaxproject.com/badge.svg :target: http://slack.pinaxproject.com/ .. image:: https://github.com/pinax/django-mailer/actions/workflows/build.yml/badge.svg :target: https://github.com/pinax/django-mailer/actions/workflows/build.yml .. 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/ django-mailer ------------- ``django-mailer`` is a reusable Django app for queuing the sending of email. It works by storing email in the database for later sending. The main reason for doing this is that for many apps, the database will be much more reliable and faster than other email sending backends which require 3rd party services e.g. SMTP or an HTTP API. By storing and sending later, we can return succeed immediately, and then attempt actual email sending in the background, with retries if needed. An additional use case is that if you are storing the mail in the same database as your normal application, the database call can participate in any ongoing transaction - that is, if the database transaction is rolled back, the email sending will also be rolled back. (In some cases this behaviour might not be desirable, so be careful). Keep in mind that file attachments are also temporarily stored in the database, which means if you are sending files larger than several hundred KB in size, you are likely to run into database limitations on how large your query can be. If this happens, you'll either need to fall back to using Django's default mail backend, or increase your database limits (a procedure that depends on which database you are using). django-mailer was developed as part of the `Pinax ecosystem `_ but is just a Django app and can be used independently of other Pinax apps. 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) Use the `-r failure` option to remove only failed log entries instead, or `-r all` to remove them all. Note that the ``send_mail`` cronjob can only run at a maximum frequency of once each minute. If a maximum delay of 60 seconds between creating an email and sending it is too much, an alternative is available. Using ``./manage.py runmailer`` a long running process is started that will check the database for new emails each ``MAILER_EMPTY_QUEUE_SLEEP`` (default: 30 seconds). Documentation and support ------------------------- See `usage.rst `_ in the docs for more advanced use cases. The Pinax documentation is available at http://pinaxproject.com/pinax/. This is an Open Source project maintained by volunteers, and outside this documentation the maintainers do not offer other support. For cases where you have found a bug you can file a GitHub issue. In case of any questions we recommend you join the `Pinax Slack team `_ and ping the Pinax team there instead of creating an issue on GitHub. You may also be able to get help on other programming sites like `Stack Overflow `_. Contribute ---------- See `CONTRIBUTING.rst `_ for information about contributing patches to ``django-mailer``. See this `blog post including a video `_, or our `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 `_ section. We also highly recommend reading our `Open Source and Self-Care blog post `_. Code of Conduct --------------- In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a `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 `_. django-mailer-2.2/RELEASE.rst000066400000000000000000000012401421270060700156740ustar00rootroot00000000000000Release process --------------- * Check that the master branching is passing all tests: https://github.com/pinax/django-mailer/actions/workflows/build.yml * 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.2/docs/000077500000000000000000000000001421270060700150155ustar00rootroot00000000000000django-mailer-2.2/docs/index.rst000066400000000000000000000001171421270060700166550ustar00rootroot00000000000000 ============= django-mailer ============= Contents: .. toctree:: usage django-mailer-2.2/docs/usage.rst000066400000000000000000000207241421270060700166600ustar00rootroot00000000000000===== 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 ==================================== The best method to explicitly send some messages through the django-mailer queue (and perhaps not others), is to use the ``connection`` parameter to the normal ``django.core.mail.send_mail`` function or the ``django.core.mail.EmailMessage`` constructor - see the Django docs as above and the `django.core.mail.get_connection `_ function. Another method to use the django-mailer queue directly, which dates from before there was such as thing as an "email backend" in Django, is to import the ``send_mail`` function (and similar) from ``mailer`` instead of from ``django.core.mail``. There is also a ``send_html_mail`` convenience function. However, we no longer guarantee that these functions will have a 100% compatible signature with the Django version, so we recommend you don't use these functions. Clear queue with command extensions =================================== With mailer in your ``INSTALLED_APPS``, there will be four 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``. * ``runmailer`` similar to ``send_mail``, but will keep running and checking the database for new messages each ``MAILER_EMPTY_QUEUE_SLEEP`` (default: 30) seconds. Can be used *instead* of ``send_mail`` to circumvent the maximum frequency of once per minute inherent to cron. * ``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. Use the ``-r failure`` option to remove only failed message logs instead, or ``-r all`` to remove them all. 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/python manage.py send_mail >> $PINAX/cron_mail.log 2>&1) 0,20,40 * * * * (cd $PINAX; /usr/local/bin/python manage.py retry_deferred >> $PINAX/cron_mail_deferred.log 2>&1) 0 0 * * * (cd $PINAX; /usr/local/bin/python 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. If you intend to use ``manage.py runmailer`` instead of ``send_mail`` it's up to you to keep this command running in the background. This can be achieved using `supervisord`_ or similar software. .. _pinax documentation: http://pinaxproject.com/docs/dev/deployment.html#sending-mail-and-notices .. _supervisord: http://supervisord.org/ 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. Error handling ============== django-mailer comes with a default error handler ``mailer.engine.handle_delivery_exception``. It marks the related message as deferred for any of these exceptions: - ``smtplib.SMTPAuthenticationError`` - ``smtplib.SMTPDataError`` - ``smtplib.SMTPRecipientsRefused`` - ``smtplib.SMTPSenderRefused`` - ``socket.error`` Any other exceptions is re-raised. That is done for backwards-compatibility as well as for flexibility: we would otherwise have to maintain an extensive and changing list of exception types, which does not scale, and you get the chance to do error handling that fits your environment like a glove. When the default behavior does not fit your environment, you can specify your own custom delivery error handler through setting ``MAILER_ERROR_HANDLER``. The value should be a string for use with Django's ``import_string``, the default is ``"mailer.engine.handle_delivery_exception"``. Your handler is passed three arguments, in order: - ``connection`` — the backend connection instance that failed delivery - ``message`` — the ``Message`` instance that failed delivery - ``exc`` — the exception instance raised by the mailer backend Your handler should return a 2-tuple of: 1. a connection instance (or ``None`` to cause a new connection to be created) 2. a string denoting the action taken by the handler, either ``"sent"`` or ``"deferred"`` precisely For an example of a custom error handler:: def my_handler(connection, message, exc): if isinstance(exc, SomeDeliveryException): # trying to re-send this very message desparately # (if you have good reason to) [..] status = 'sent' elif isinstance(exc, SomeOtherException): message.defer() connection = None # i.e. ask for a new connection status = 'deferred' else: six.reraise(*sys.exc_info()) return connection, status 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 disable the file-based locking, you can set the ``MAILER_USE_FILE_LOCK`` setting to ``False``. 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. However, note that it's actually only used when directly sending messages through ``mailer.send_mail``, 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.2/manage.py000077500000000000000000000010711421270060700156710ustar00rootroot00000000000000#!/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.2/release.sh000077500000000000000000000005151421270060700160450ustar00rootroot00000000000000#!/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.2/requirements-dev.txt000066400000000000000000000000351421270060700201230ustar00rootroot00000000000000flake8==3.8.4 check-manifest django-mailer-2.2/requirements-test.txt000066400000000000000000000001031421270060700203200ustar00rootroot00000000000000mock==3.0.5;python_version<'3.6' mock==4.0.1;python_version>='3.6' django-mailer-2.2/runtests.py000077500000000000000000000020771421270060700163370ustar00rootroot00000000000000#!/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) warnings.simplefilter("always", PendingDeprecationWarning) 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.2/setup.cfg000066400000000000000000000001441421270060700157050ustar00rootroot00000000000000[bdist_wheel] universal = 1 [flake8] max-line-length=100 exclude=src/mailer/migrations,build,.tox django-mailer-2.2/setup.py000077500000000000000000000033731421270060700156100ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup, find_packages setup( name="django-mailer", version="2.2", description="A reusable Django app for queuing the sending of email", long_description="""``django-mailer`` is a reusable Django app for queuing the sending of email. It works by storing email in the database for later sending. The main reason for doing this is that for many apps, the database will be much more reliable and faster than other email sending backends which require 3rd party services e.g. SMTP or an HTTP API. By storing and sending later, we can return succeed immediately, and then attempt actual email sending in the background, with retries if needed.""", 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", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Framework :: Django", ], install_requires=[ 'Django >= 1.11', 'lockfile >= 0.8', 'six', ], ) django-mailer-2.2/src/000077500000000000000000000000001421270060700146545ustar00rootroot00000000000000django-mailer-2.2/src/mailer/000077500000000000000000000000001421270060700161255ustar00rootroot00000000000000django-mailer-2.2/src/mailer/__init__.py000066400000000000000000000075721421270060700202510ustar00rootroot00000000000000from __future__ import absolute_import import warnings import six __version__ = '2.2' 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): if six.PY2: # Only runs Django 1.11 from django.utils.encoding import force_unicode as force_str else: from django.utils.encoding import force_str from mailer.models import make_message priority = get_priority(priority) # need to do this in case subject used lazy version of ugettext subject = force_str(subject) message = force_str(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 """ if six.PY2: # Only runs Django 1.11 from django.utils.encoding import force_unicode as force_str else: from django.utils.encoding import force_str 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_str(subject) message = force_str(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 if six.PY2: # Only runs Django 1.11 from django.utils.encoding import force_unicode as force_str else: from django.utils.encoding import force_str return send_mail(settings.EMAIL_SUBJECT_PREFIX + force_str(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 if six.PY2: # Only runs Django 1.11 from django.utils.encoding import force_unicode as force_str else: from django.utils.encoding import force_str return send_mail(settings.EMAIL_SUBJECT_PREFIX + force_str(subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]) django-mailer-2.2/src/mailer/admin.py000066400000000000000000000023221421270060700175660ustar00rootroot00000000000000from __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.2/src/mailer/apps.py000066400000000000000000000002201421270060700174340ustar00rootroot00000000000000from django.apps import AppConfig class MailerConfig(AppConfig): name = 'mailer' default_auto_field = 'django.db.models.BigAutoField' django-mailer-2.2/src/mailer/backend.py000066400000000000000000000010451421270060700200660ustar00rootroot00000000000000from __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.2/src/mailer/engine.py000066400000000000000000000231061421270060700177460ustar00rootroot00000000000000from __future__ import unicode_literals import contextlib import logging import smtplib import sys import time from socket import error as socket_error import lockfile import six from django import VERSION as DJANGO_VERSION 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.core.mail.utils import DNS_NAME from django.db import DatabaseError, NotSupportedError, OperationalError, transaction from django.utils.module_loading import import_string from mailer.models import (RESULT_FAILURE, RESULT_SUCCESS, Message, MessageLog, get_message_id) if DJANGO_VERSION[0] >= 2: NotSupportedFeatureException = NotSupportedError else: NotSupportedFeatureException = DatabaseError # 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) logger = logging.getLogger(__name__) 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 NotSupportedFeatureException: # 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: # Use cached DNS_NAME for performance msg.extra_headers['Message-ID'] = make_msgid(domain=DNS_NAME) 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: logger.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: logger.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: logger.debug("Throttling email delivery. " "Sleeping %s seconds", EMAIL_THROTTLE) time.sleep(EMAIL_THROTTLE) def handle_delivery_exception(connection, message, exc): if isinstance(exc, (smtplib.SMTPAuthenticationError, smtplib.SMTPDataError, smtplib.SMTPRecipientsRefused, smtplib.SMTPSenderRefused, socket_error)): message.defer() logger.info("message deferred due to failure: %s" % exc) MessageLog.objects.log(message, RESULT_FAILURE, log_message=str(exc)) connection = None # i.e. enforce creation of a new connection status = 'deferred' return connection, status # The idea is (1) to be backwards compatible with existing behavior # and (2) not have delivery errors go unnoticed six.reraise(*sys.exc_info()) def acquire_lock(): logger.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: logger.error("lock already in place. quitting.") return False, lock except lockfile.LockTimeout: logger.error("waiting for the lock timed out. quitting.") return False, lock logger.debug("acquired.") return True, lock def release_lock(lock): logger.debug("releasing lock...") lock.release() logger.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" ) # allows disabling file locking. The default is True use_file_lock = getattr(settings, "MAILER_USE_FILE_LOCK", True) error_handler = import_string( getattr(settings, 'MAILER_ERROR_HANDLER', 'mailer.engine.handle_delivery_exception') ) _require_no_backend_loop(mailer_email_backend) if use_file_lock: acquired, lock = acquire_lock() if not acquired: return start_time = time.time() counts = {'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) logger.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) counts['sent'] += 1 else: logger.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 Exception as err: connection, action_taken = error_handler(connection, message, err) counts[action_taken] += 1 # Check if we reached the limits for the current run if _limits_reached(counts['sent'], counts['deferred']): break _throttle_emails() finally: if use_file_lock: release_lock(lock) logger.info("Sent", extra={ 'sent': counts['sent'], 'deferred': counts['deferred'], 'duration': 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.non_deferred().exists(): logger.debug("sleeping for %s seconds before checking queue again" % EMPTY_QUEUE_SLEEP) time.sleep(EMPTY_QUEUE_SLEEP) send_all() django-mailer-2.2/src/mailer/locale/000077500000000000000000000000001421270060700173645ustar00rootroot00000000000000django-mailer-2.2/src/mailer/locale/ja/000077500000000000000000000000001421270060700177565ustar00rootroot00000000000000django-mailer-2.2/src/mailer/locale/ja/LC_MESSAGES/000077500000000000000000000000001421270060700215435ustar00rootroot00000000000000django-mailer-2.2/src/mailer/locale/ja/LC_MESSAGES/django.po000066400000000000000000000022141421270060700233440ustar00rootroot00000000000000# 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.2/src/mailer/locale/ru/000077500000000000000000000000001421270060700200125ustar00rootroot00000000000000django-mailer-2.2/src/mailer/locale/ru/LC_MESSAGES/000077500000000000000000000000001421270060700215775ustar00rootroot00000000000000django-mailer-2.2/src/mailer/locale/ru/LC_MESSAGES/django.po000066400000000000000000000022231421270060700234000ustar00rootroot00000000000000# 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.2/src/mailer/management/000077500000000000000000000000001421270060700202415ustar00rootroot00000000000000django-mailer-2.2/src/mailer/management/__init__.py000066400000000000000000000000001421270060700223400ustar00rootroot00000000000000django-mailer-2.2/src/mailer/management/commands/000077500000000000000000000000001421270060700220425ustar00rootroot00000000000000django-mailer-2.2/src/mailer/management/commands/__init__.py000066400000000000000000000000001421270060700241410ustar00rootroot00000000000000django-mailer-2.2/src/mailer/management/commands/purge_mail_log.py000066400000000000000000000016411421270060700254030ustar00rootroot00000000000000import logging from django.core.management.base import BaseCommand from mailer.models import MessageLog, RESULT_SUCCESS, RESULT_FAILURE RESULT_CODES = { 'success': [RESULT_SUCCESS], 'failure': [RESULT_FAILURE], 'all': [RESULT_SUCCESS, RESULT_FAILURE] } logger = logging.getLogger(__name__) class Command(BaseCommand): help = "Delete mailer log" def add_arguments(self, parser): parser.add_argument('days', type=int) parser.add_argument('-r', '--result', choices=RESULT_CODES.keys(), help='Delete logs of messages with the given result code(s) ' '(default: success)') def handle(self, *args, **options): days = options['days'] result_codes = RESULT_CODES.get(options['result']) count = MessageLog.objects.purge_old_entries(days, result_codes) logger.info("%s log entries deleted " % count) django-mailer-2.2/src/mailer/management/commands/retry_deferred.py000066400000000000000000000013101421270060700254140ustar00rootroot00000000000000import logging import warnings from django.core.management.base import BaseCommand from mailer.models import Message from mailer.management.helpers import CronArgMixin logger = logging.getLogger(__name__) class Command(CronArgMixin, BaseCommand): help = "Attempt to resend any deferred mail." def handle(self, *args, **options): if options['cron']: warnings.warn("retry_deferred's -c/--cron option is no longer " "necessary and will be removed in a future release", DeprecationWarning) count = Message.objects.retry_deferred() # @@@ new_priority not yet supported logger.info("%s message(s) retried" % count) django-mailer-2.2/src/mailer/management/commands/runmailer.py000066400000000000000000000010341421270060700244100ustar00rootroot00000000000000import sys from datetime import datetime from django.core.management import BaseCommand from mailer.engine import send_loop class Command(BaseCommand): """Start the django-mailer send loop""" def handle(self, *args, **options): self.stdout.write(datetime.now().strftime('%B %d, %Y - %X')) self.stdout.write('Starting django-mailer send loop.') quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' self.stdout.write('Quit the loop with %s.' % quit_command) send_loop() django-mailer-2.2/src/mailer/management/commands/send_mail.py000066400000000000000000000017021421270060700243470ustar00rootroot00000000000000import logging import warnings 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) logger = logging.getLogger(__name__) 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: warnings.warn("send_mail's -c/--cron option is no longer " "necessary and will be removed in a future release", DeprecationWarning) logger.info("-" * 72) # if PAUSE_SEND is turned on don't do anything. if not PAUSE_SEND: send_all() else: logger.info("sending is paused, quitting.") django-mailer-2.2/src/mailer/management/helpers.py000066400000000000000000000013521421270060700222560ustar00rootroot00000000000000from 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.2/src/mailer/migrations/000077500000000000000000000000001421270060700203015ustar00rootroot00000000000000django-mailer-2.2/src/mailer/migrations/0001_initial.py000066400000000000000000000043611421270060700227500ustar00rootroot00000000000000# -*- 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.2/src/mailer/migrations/0002_auto_20150720_1433.py000066400000000000000000000012621421270060700237170ustar00rootroot00000000000000# -*- 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.2/src/mailer/migrations/0003_messagelog_message_id.py000066400000000000000000000007231421270060700256250ustar00rootroot00000000000000# -*- 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.2/src/mailer/migrations/0004_auto_20190920_1512.py000066400000000000000000000016531421270060700237310ustar00rootroot00000000000000# 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')]), ), 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.2/src/mailer/migrations/0005_id_bigautofield.py000066400000000000000000000015521421270060700244340ustar00rootroot00000000000000# Generated by Django 3.2.9 on 2021-11-04 13:31 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('mailer', '0004_auto_20190920_1512'), ] operations = [ migrations.AlterField( model_name='dontsendentry', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='message', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='messagelog', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ] django-mailer-2.2/src/mailer/migrations/__init__.py000066400000000000000000000000001421270060700224000ustar00rootroot00000000000000django-mailer-2.2/src/mailer/models.py000066400000000000000000000225561421270060700177740ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import logging import pickle import datetime import six 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 if six.PY2: from django.utils.translation import ugettext_lazy as _ else: from django.utils.translation import gettext_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) logger = logging.getLogger(__name__) class BigAutoModel(models.Model): # default_auto_field = 'django.db.models.BigAutoField' isn't supported # by Django < 3.2. Use an explicit field definition instead. # This workaround can be removed once support for Django < 3.2 is dropped. id = models.BigAutoField(auto_created=True, primary_key=True, verbose_name='ID') class Meta: abstract = True 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(BigAutoModel): """ The email stored for later sending. """ # 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): logger.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(BigAutoModel): 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, result_codes=None): if result_codes is None: # retro-compatibility with previous versions result_codes = [RESULT_SUCCESS] limit = datetime_now() - datetime.timedelta(days=days) query = self.filter(when_attempted__lt=limit, result__in=result_codes) count = query.count() query.delete() return count @python_2_unicode_compatible class MessageLog(BigAutoModel): """ A log entry which stores the result (and optionally a log message) for an attempt to send a Message. """ # 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.2/tests/000077500000000000000000000000001421270060700152275ustar00rootroot00000000000000django-mailer-2.2/tests/__init__.py000066400000000000000000000014311421270060700173370ustar00rootroot00000000000000import smtplib from django.core.mail.backends.locmem import EmailBackend as LocMemEmailBackend 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") django-mailer-2.2/tests/test_mailer.py000066400000000000000000000746121421270060700201230ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime import pickle import time import django import lockfile from django.core import mail from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.test import TestCase from django.utils.timezone import now as datetime_now from mock import 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) from . import TestMailerEmailBackend 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): def send_mail(success): backend = ("django.core.mail.backends.locmem.EmailBackend" if success else "tests.FailingMailerEmailBackend") with self.settings(MAILER_EMAIL_BACKEND=backend): mailer.send_mail("Subject", "Body", "sender@example.com", ["recipient@example.com"]) engine.send_all() if not success: Message.objects.retry_deferred() engine.send_all() # 1 success, 1 failure, and purge only success send_mail(True) send_mail(False) 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) # 1 success, 1 failure, and purge only failures send_mail(True) 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', '-r', 'failure') self.assertEqual(MessageLog.objects.filter(result=RESULT_FAILURE).count(), 0) self.assertNotEqual(MessageLog.objects.filter(result=RESULT_SUCCESS).count(), 0) # 1 success, 1 failure, and purge everything send_mail(False) 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', '-r', 'all') self.assertEqual(MessageLog.objects.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: mailer.send_mail("Subject", "Body", "deferred@example.com", ["rec@example.com"], priority=PRIORITY_DEFERRED) 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 with patch('mailer.engine.logger.warning') as mock_warning: # 2 will get deferred 3 remain undeferred engine.send_all() mock_warning.assert_called_once() arg = mock_warning.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('mailer.engine.logger.warning') as mock_warning: engine.send_all() mock_warning.assert_called_once() arg = mock_warning.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 # --cron/c command option is deprecated return call_command(command, '--cron={}'.format(cron_value)) class CommandHelperTest(TestCase): def test_send_mail_no_cron(self): call_command('send_mail') def test_send_mail_cron_0(self): # deprecated call_command_with_cron_arg('send_mail', 0) def test_send_mail_cron_1(self): # deprecated call_command_with_cron_arg('send_mail', 1) def test_retry_deferred_no_cron(self): call_command('retry_deferred') def test_retry_deferred_cron_0(self): # deprecated call_command_with_cron_arg('retry_deferred', 0) def test_retry_deferred_cron_1(self): # deprecated call_command_with_cron_arg('retry_deferred', 1) 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)) class UseFileLockTest(TestCase): """Test the MAILER_USE_FILE_LOCK setting.""" def setUp(self): # mocking return_value to prevent "ValueError: not enough values to unpack" self.patcher_acquire_lock = patch("mailer.engine.acquire_lock", return_value=(True, True)) self.patcher_release_lock = patch("mailer.engine.release_lock", return_value=(True, True)) self.mock_acquire_lock = self.patcher_acquire_lock.start() self.mock_release_lock = self.patcher_release_lock.start() def test_mailer_use_file_lock_enabled(self): with self.settings(MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): engine.send_all() self.mock_acquire_lock.assert_called_once() self.mock_release_lock.assert_called_once() def test_mailer_use_file_lock_disabled(self): with self.settings(MAILER_USE_FILE_LOCK=False, MAILER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend"): engine.send_all() self.mock_acquire_lock.assert_not_called() self.mock_release_lock.assert_not_called() def tearDown(self): self.patcher_acquire_lock.stop() self.patcher_release_lock.stop() django-mailer-2.2/tox.ini000066400000000000000000000017731421270060700154100ustar00rootroot00000000000000[tox] # Remember to add to .github/workflows/build.yml if this is added to. envlist = # Not every combination, just a representative sample: py27-django111-test, {py35,py36}-django20-test, py37-django21-test, py38-django22-test, py39-django{30,31,32,40}-test, py310-django40-test, flake-py39-django32, checkmanifest-py39, [testenv] basepython = py27: python2.7 py35: python3.5 py36: python3.6 py37: python3.7 py38: python3.8 py39: python3.9 py310: python3.10 commands = test: coverage run ./runtests.py flake: flake8 --statistics --benchmark checkmanifest: check-manifest deps = coverage -r requirements-test.txt -e . flake8: -r requirements-dev.txt checkmanifest: -r requirements-dev.txt django111: Django==1.11.29 django20: Django==2.0.13 django21: Django==2.1.15 django22: Django==2.2.19 django30: Django==3.0.14 django31: Django==3.1.12 django32: Django==3.2.4 django40: Django==4.0.3