django-celery-email-3.0.0/ 0000775 0003720 0003720 00000000000 13574205771 016177 5 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/MANIFEST.in 0000664 0003720 0003720 00000000223 13574205644 017731 0 ustar travis travis 0000000 0000000 include README.rst include LICENSE include MANIFEST.in include requirements.txt recursive-include tests *.py recursive-include djcelery_email *.py django-celery-email-3.0.0/requirements.txt 0000664 0003720 0003720 00000000072 13574205644 021461 0 ustar travis travis 0000000 0000000 Django>=2.2 celery>=4.0 django-appconf flake8 twine wheel django-celery-email-3.0.0/PKG-INFO 0000664 0003720 0003720 00000024051 13574205771 017276 0 ustar travis travis 0000000 0000000 Metadata-Version: 1.1 Name: django-celery-email Version: 3.0.0 Summary: An async Django email backend using celery Home-page: https://github.com/pmclanahan/django-celery-email Author: Paul McLanahan Author-email: paul@mclanahan.net License: BSD Description: ========================================================== django-celery-email - A Celery-backed Django Email Backend ========================================================== .. image:: https://img.shields.io/travis/pmclanahan/django-celery-email/master.svg :target: https://travis-ci.org/pmclanahan/django-celery-email .. image:: https://img.shields.io/pypi/v/django-celery-email.svg :target: https://pypi.python.org/pypi/django-celery-email A `Django`_ email backend that uses a `Celery`_ queue for out-of-band sending of the messages. .. _`Celery`: http://celeryproject.org/ .. _`Django`: http://www.djangoproject.org/ .. warning:: This version requires the following versions: * Python >= 3.5 * Django 2.2, and 3.0 * Celery 4.0 Using django-celery-email ========================= To enable ``django-celery-email`` for your project you need to add ``djcelery_email`` to ``INSTALLED_APPS``:: INSTALLED_APPS += ("djcelery_email",) You must then set ``django-celery-email`` as your ``EMAIL_BACKEND``:: EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' By default ``django-celery-email`` will use Django's builtin ``SMTP`` email backend for the actual sending of the mail. If you'd like to use another backend, you may set it in ``CELERY_EMAIL_BACKEND`` just like you would normally have set ``EMAIL_BACKEND`` before you were using Celery. In fact, the normal installation procedure will most likely be to get your email working using only Django, then change ``EMAIL_BACKEND`` to ``CELERY_EMAIL_BACKEND``, and then add the new ``EMAIL_BACKEND`` setting from above. Mass email are sent in chunks of size ``CELERY_EMAIL_CHUNK_SIZE`` (defaults to 10). If you need to set any of the settings (attributes) you'd normally be able to set on a `Celery Task`_ class had you written it yourself, you may specify them in a ``dict`` in the ``CELERY_EMAIL_TASK_CONFIG`` setting:: CELERY_EMAIL_TASK_CONFIG = { 'queue' : 'email', 'rate_limit' : '50/m', # * CELERY_EMAIL_CHUNK_SIZE (default: 10) ... } There are some default settings. Unless you specify otherwise, the equivalent of the following settings will apply:: CELERY_EMAIL_TASK_CONFIG = { 'name': 'djcelery_email_send', 'ignore_result': True, } After this setup is complete, and you have a working Celery install, sending email will work exactly like it did before, except that the sending will be handled by your Celery workers:: from django.core import mail emails = ( ('Hey Man', "I'm The Dude! So that's what you call me.", 'dude@aol.com', ['mr@lebowski.com']), ('Dammit Walter', "Let's go bowlin'.", 'dude@aol.com', ['wsobchak@vfw.org']), ) results = mail.send_mass_mail(emails) ``results`` will be a list of celery `AsyncResult`_ objects that you may ignore, or use to check the status of the email delivery task, or even wait for it to complete if want. You have to enable a result backend and set ``ignore_result`` to ``False`` in ``CELERY_EMAIL_TASK_CONFIG`` if you want to use these. You should also set ``CELERY_EMAIL_CHUNK_SIZE = 1`` in settings if you are concerned about task status and results. See the `Celery docs`_ for more info. ``len(results)`` will be the number of emails you attempted to send divided by CELERY_EMAIL_CHUNK_SIZE, and is in no way a reflection on the success or failure of their delivery. .. _`Celery Task`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#basics .. _`Celery docs`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#task-states .. _`AsyncResult`: http://celery.readthedocs.org/en/latest/reference/celery.result.html#celery.result.AsyncResult Changelog ========= 3.0.0 - 2019.12.10 ------------------ * Support for Django 3.0 * Support for Python 3.8 * Droppped support for Django 1.x, Django 2.0 and Django 2.1 * Droppped support for Python 2.7 2.0.2 - 2019.05.29 ------------------ * Reduce memory usage by running email_to_dict on chunks. Thanks `Paul Brown`_. * Simplify dict_to_email for readability and efficiency. Thanks `Paul Brown`_. * Update test matrix for supported versions of Django, Celery and Python. Thanks `James`_. .. _Paul Brown: https://github.com/pawl .. _James: https://github.com/jmsmkn 2.0.1 - 2018.18.27 ------------------ * Fix bug preventing sending text/* encoded mime attachments. Thanks `Cesar Canassa`_. .. _Cesar Canassa: https://github.com/canassa 2.0 - 2017.07.10 ---------------- * Support for Django 1.11 and Celery 4.0 * Dropped support for Celery 2.x and 3.x * Dropped support for Python 3.3 1.1.5 - 2016.07.20 ------------------ * Support extra email attributes via CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES setting * Updated version requirements in README 1.1.4 - 2016.01.19 ------------------ * Support sending email with embedded images. Thanks `Georg Zimmer`_. * Document CELERY_EMAIL_CHUNK_SIZE. Thanks `Jonas Haag`_. * Add exception handling to email backend connection. Thanks `Tom`_. .. _Georg Zimmer: https://github.com/georgmzimmer .. _Tom: https://github.com/tomleo 1.1.3 - 2015.11.06 ------------------ * Support setting celery.base from string. Thanks `Matthew Jacobi`_. * Use six for py2/3 string compatibility. Thanks `Matthew Jacobi`_. * Pass content_subtype back in for retries. Thanks `Mark Joshua Tan`_. * Rework how tests work, add tox, rework travis-ci matrix. * Use six from django.utils. * Release a universal wheel. .. _Matthew Jacobi: https://github.com/oppianmatt .. _Mark Joshua Tan: https://github.com/mark-tan 1.1.2 - 2015.07.06 ------------------ * Fix for HTML-only emails. Thanks `gnarvaja`_. .. _gnarvaja: https://github.com/gnarvaja 1.1.1 - 2015.03.20 ------------------ * Fix for backward compatibility of task kwarg handling - Thanks `Jeremy Thurgood`_. .. _Jeremy Thurgood: https://github.com/jerith 1.1.0 - 2015.03.06 ------------------ * New PyPI release rolling up 1.0.5 changes and some cleanup. * More backward compatability in task. Will still accept message objects and lists of message objects. * Thanks again to everyone who contributed to 1.0.5. 1.0.5 - 2014.08.24 ------------------ * Django 1.6 support, Travis CI testing, chunked sending & more - thanks `Jonas Haag`_. * HTML email support - thanks `Andres Riancho`_. * Support for JSON transit for Celery, sponsored by `DigiACTive`_. * Drop support for Django 1.2. .. _`Jonas Haag`: https://github.com/jonashaag .. _`Andres Riancho`: https://github.com/andresriancho .. _`DigiACTive`: https://github.com/digiactive 1.0.4 - 2013.10.12 ------------------ * Add Django 1.5.2 and Python 3 support. * Thanks to `Stefan Wehrmeyer`_ for the contribution. .. _`Stefan Wehrmeyer`: https://github.com/stefanw 1.0.3 - 2012.03.06 ------------------ * Backend will now pass any kwargs with which it is initialized to the email sending backend. * Thanks to `Fedor Tyurin`_ for the contribution. .. _`Fedor Tyurin`: https://bitbucket.org/ftyurin 1.0.2 - 2012.02.21 ------------------ * Task and backend now accept kwargs that can be used in signal handlers. * Task now returns the result from the email sending backend. * Thanks to `Yehonatan Daniv`_ for these changes. .. _`Yehonatan Daniv`: https://bitbucket.org/ydaniv 1.0.1 - 2011.10.06 ------------------ * Fixed a bug that resulted in tasks that were throwing errors reporting success. * If there is an exception thrown by the sending email backend, the result of the task will now be this exception. Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Topic :: Communications Classifier: Topic :: Communications :: Email Classifier: Topic :: System :: Distributed Computing Classifier: Topic :: Software Development :: Libraries :: Python Modules django-celery-email-3.0.0/django_celery_email.egg-info/ 0000775 0003720 0003720 00000000000 13574205771 023645 5 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/django_celery_email.egg-info/PKG-INFO 0000664 0003720 0003720 00000024051 13574205771 024744 0 ustar travis travis 0000000 0000000 Metadata-Version: 1.1 Name: django-celery-email Version: 3.0.0 Summary: An async Django email backend using celery Home-page: https://github.com/pmclanahan/django-celery-email Author: Paul McLanahan Author-email: paul@mclanahan.net License: BSD Description: ========================================================== django-celery-email - A Celery-backed Django Email Backend ========================================================== .. image:: https://img.shields.io/travis/pmclanahan/django-celery-email/master.svg :target: https://travis-ci.org/pmclanahan/django-celery-email .. image:: https://img.shields.io/pypi/v/django-celery-email.svg :target: https://pypi.python.org/pypi/django-celery-email A `Django`_ email backend that uses a `Celery`_ queue for out-of-band sending of the messages. .. _`Celery`: http://celeryproject.org/ .. _`Django`: http://www.djangoproject.org/ .. warning:: This version requires the following versions: * Python >= 3.5 * Django 2.2, and 3.0 * Celery 4.0 Using django-celery-email ========================= To enable ``django-celery-email`` for your project you need to add ``djcelery_email`` to ``INSTALLED_APPS``:: INSTALLED_APPS += ("djcelery_email",) You must then set ``django-celery-email`` as your ``EMAIL_BACKEND``:: EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' By default ``django-celery-email`` will use Django's builtin ``SMTP`` email backend for the actual sending of the mail. If you'd like to use another backend, you may set it in ``CELERY_EMAIL_BACKEND`` just like you would normally have set ``EMAIL_BACKEND`` before you were using Celery. In fact, the normal installation procedure will most likely be to get your email working using only Django, then change ``EMAIL_BACKEND`` to ``CELERY_EMAIL_BACKEND``, and then add the new ``EMAIL_BACKEND`` setting from above. Mass email are sent in chunks of size ``CELERY_EMAIL_CHUNK_SIZE`` (defaults to 10). If you need to set any of the settings (attributes) you'd normally be able to set on a `Celery Task`_ class had you written it yourself, you may specify them in a ``dict`` in the ``CELERY_EMAIL_TASK_CONFIG`` setting:: CELERY_EMAIL_TASK_CONFIG = { 'queue' : 'email', 'rate_limit' : '50/m', # * CELERY_EMAIL_CHUNK_SIZE (default: 10) ... } There are some default settings. Unless you specify otherwise, the equivalent of the following settings will apply:: CELERY_EMAIL_TASK_CONFIG = { 'name': 'djcelery_email_send', 'ignore_result': True, } After this setup is complete, and you have a working Celery install, sending email will work exactly like it did before, except that the sending will be handled by your Celery workers:: from django.core import mail emails = ( ('Hey Man', "I'm The Dude! So that's what you call me.", 'dude@aol.com', ['mr@lebowski.com']), ('Dammit Walter', "Let's go bowlin'.", 'dude@aol.com', ['wsobchak@vfw.org']), ) results = mail.send_mass_mail(emails) ``results`` will be a list of celery `AsyncResult`_ objects that you may ignore, or use to check the status of the email delivery task, or even wait for it to complete if want. You have to enable a result backend and set ``ignore_result`` to ``False`` in ``CELERY_EMAIL_TASK_CONFIG`` if you want to use these. You should also set ``CELERY_EMAIL_CHUNK_SIZE = 1`` in settings if you are concerned about task status and results. See the `Celery docs`_ for more info. ``len(results)`` will be the number of emails you attempted to send divided by CELERY_EMAIL_CHUNK_SIZE, and is in no way a reflection on the success or failure of their delivery. .. _`Celery Task`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#basics .. _`Celery docs`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#task-states .. _`AsyncResult`: http://celery.readthedocs.org/en/latest/reference/celery.result.html#celery.result.AsyncResult Changelog ========= 3.0.0 - 2019.12.10 ------------------ * Support for Django 3.0 * Support for Python 3.8 * Droppped support for Django 1.x, Django 2.0 and Django 2.1 * Droppped support for Python 2.7 2.0.2 - 2019.05.29 ------------------ * Reduce memory usage by running email_to_dict on chunks. Thanks `Paul Brown`_. * Simplify dict_to_email for readability and efficiency. Thanks `Paul Brown`_. * Update test matrix for supported versions of Django, Celery and Python. Thanks `James`_. .. _Paul Brown: https://github.com/pawl .. _James: https://github.com/jmsmkn 2.0.1 - 2018.18.27 ------------------ * Fix bug preventing sending text/* encoded mime attachments. Thanks `Cesar Canassa`_. .. _Cesar Canassa: https://github.com/canassa 2.0 - 2017.07.10 ---------------- * Support for Django 1.11 and Celery 4.0 * Dropped support for Celery 2.x and 3.x * Dropped support for Python 3.3 1.1.5 - 2016.07.20 ------------------ * Support extra email attributes via CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES setting * Updated version requirements in README 1.1.4 - 2016.01.19 ------------------ * Support sending email with embedded images. Thanks `Georg Zimmer`_. * Document CELERY_EMAIL_CHUNK_SIZE. Thanks `Jonas Haag`_. * Add exception handling to email backend connection. Thanks `Tom`_. .. _Georg Zimmer: https://github.com/georgmzimmer .. _Tom: https://github.com/tomleo 1.1.3 - 2015.11.06 ------------------ * Support setting celery.base from string. Thanks `Matthew Jacobi`_. * Use six for py2/3 string compatibility. Thanks `Matthew Jacobi`_. * Pass content_subtype back in for retries. Thanks `Mark Joshua Tan`_. * Rework how tests work, add tox, rework travis-ci matrix. * Use six from django.utils. * Release a universal wheel. .. _Matthew Jacobi: https://github.com/oppianmatt .. _Mark Joshua Tan: https://github.com/mark-tan 1.1.2 - 2015.07.06 ------------------ * Fix for HTML-only emails. Thanks `gnarvaja`_. .. _gnarvaja: https://github.com/gnarvaja 1.1.1 - 2015.03.20 ------------------ * Fix for backward compatibility of task kwarg handling - Thanks `Jeremy Thurgood`_. .. _Jeremy Thurgood: https://github.com/jerith 1.1.0 - 2015.03.06 ------------------ * New PyPI release rolling up 1.0.5 changes and some cleanup. * More backward compatability in task. Will still accept message objects and lists of message objects. * Thanks again to everyone who contributed to 1.0.5. 1.0.5 - 2014.08.24 ------------------ * Django 1.6 support, Travis CI testing, chunked sending & more - thanks `Jonas Haag`_. * HTML email support - thanks `Andres Riancho`_. * Support for JSON transit for Celery, sponsored by `DigiACTive`_. * Drop support for Django 1.2. .. _`Jonas Haag`: https://github.com/jonashaag .. _`Andres Riancho`: https://github.com/andresriancho .. _`DigiACTive`: https://github.com/digiactive 1.0.4 - 2013.10.12 ------------------ * Add Django 1.5.2 and Python 3 support. * Thanks to `Stefan Wehrmeyer`_ for the contribution. .. _`Stefan Wehrmeyer`: https://github.com/stefanw 1.0.3 - 2012.03.06 ------------------ * Backend will now pass any kwargs with which it is initialized to the email sending backend. * Thanks to `Fedor Tyurin`_ for the contribution. .. _`Fedor Tyurin`: https://bitbucket.org/ftyurin 1.0.2 - 2012.02.21 ------------------ * Task and backend now accept kwargs that can be used in signal handlers. * Task now returns the result from the email sending backend. * Thanks to `Yehonatan Daniv`_ for these changes. .. _`Yehonatan Daniv`: https://bitbucket.org/ydaniv 1.0.1 - 2011.10.06 ------------------ * Fixed a bug that resulted in tasks that were throwing errors reporting success. * If there is an exception thrown by the sending email backend, the result of the task will now be this exception. Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Topic :: Communications Classifier: Topic :: Communications :: Email Classifier: Topic :: System :: Distributed Computing Classifier: Topic :: Software Development :: Libraries :: Python Modules django-celery-email-3.0.0/django_celery_email.egg-info/dependency_links.txt 0000664 0003720 0003720 00000000001 13574205771 027713 0 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/django_celery_email.egg-info/top_level.txt 0000664 0003720 0003720 00000000017 13574205771 026375 0 ustar travis travis 0000000 0000000 djcelery_email django-celery-email-3.0.0/django_celery_email.egg-info/not-zip-safe 0000664 0003720 0003720 00000000001 13574205647 026075 0 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/django_celery_email.egg-info/SOURCES.txt 0000664 0003720 0003720 00000001047 13574205771 025533 0 ustar travis travis 0000000 0000000 LICENSE MANIFEST.in README.rst requirements.txt setup.cfg setup.py django_celery_email.egg-info/PKG-INFO django_celery_email.egg-info/SOURCES.txt django_celery_email.egg-info/dependency_links.txt django_celery_email.egg-info/not-zip-safe django_celery_email.egg-info/requires.txt django_celery_email.egg-info/top_level.txt djcelery_email/__about__.py djcelery_email/__init__.py djcelery_email/backends.py djcelery_email/conf.py djcelery_email/models.py djcelery_email/tasks.py djcelery_email/utils.py tests/__init__.py tests/settings.py tests/tests.py django-celery-email-3.0.0/django_celery_email.egg-info/requires.txt 0000664 0003720 0003720 00000000047 13574205771 026246 0 ustar travis travis 0000000 0000000 django>=2.2 celery>=4.0 django-appconf django-celery-email-3.0.0/setup.cfg 0000664 0003720 0003720 00000000075 13574205771 020022 0 ustar travis travis 0000000 0000000 [wheel] universal = 1 [egg_info] tag_build = tag_date = 0 django-celery-email-3.0.0/djcelery_email/ 0000775 0003720 0003720 00000000000 13574205771 021147 5 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/djcelery_email/conf.py 0000664 0003720 0003720 00000000401 13574205644 022440 0 ustar travis travis 0000000 0000000 from appconf import AppConf class DjangoCeleryEmailAppConf(AppConf): class Meta: prefix = 'CELERY_EMAIL' TASK_CONFIG = {} BACKEND = 'django.core.mail.backends.smtp.EmailBackend' CHUNK_SIZE = 10 MESSAGE_EXTRA_ATTRIBUTES = None django-celery-email-3.0.0/djcelery_email/backends.py 0000664 0003720 0003720 00000001326 13574205644 023274 0 ustar travis travis 0000000 0000000 from django.conf import settings from django.core.mail.backends.base import BaseEmailBackend from djcelery_email.tasks import send_emails from djcelery_email.utils import chunked, email_to_dict class CeleryEmailBackend(BaseEmailBackend): def __init__(self, fail_silently=False, **kwargs): super(CeleryEmailBackend, self).__init__(fail_silently) self.init_kwargs = kwargs def send_messages(self, email_messages): result_tasks = [] for chunk in chunked(email_messages, settings.CELERY_EMAIL_CHUNK_SIZE): chunk_messages = [email_to_dict(msg) for msg in chunk] result_tasks.append(send_emails.delay(chunk_messages, self.init_kwargs)) return result_tasks django-celery-email-3.0.0/djcelery_email/utils.py 0000664 0003720 0003720 00000007644 13574205644 022673 0 ustar travis travis 0000000 0000000 import copy import base64 from email.mime.base import MIMEBase from django.conf import settings from django.core.mail import EmailMultiAlternatives, EmailMessage def chunked(iterator, chunksize): """ Yields items from 'iterator' in chunks of size 'chunksize'. >>> list(chunked([1, 2, 3, 4, 5], chunksize=2)) [(1, 2), (3, 4), (5,)] """ chunk = [] for idx, item in enumerate(iterator, 1): chunk.append(item) if idx % chunksize == 0: yield chunk chunk = [] if chunk: yield chunk def email_to_dict(message): if isinstance(message, dict): return message message_dict = {'subject': message.subject, 'body': message.body, 'from_email': message.from_email, 'to': message.to, 'bcc': message.bcc, # ignore connection 'attachments': [], 'headers': message.extra_headers, 'cc': message.cc, 'reply_to': message.reply_to} if hasattr(message, 'alternatives'): message_dict['alternatives'] = message.alternatives if message.content_subtype != EmailMessage.content_subtype: message_dict["content_subtype"] = message.content_subtype if message.mixed_subtype != EmailMessage.mixed_subtype: message_dict["mixed_subtype"] = message.mixed_subtype attachments = message.attachments for attachment in attachments: if isinstance(attachment, MIMEBase): filename = attachment.get_filename('') binary_contents = attachment.get_payload(decode=True) mimetype = attachment.get_content_type() else: filename, binary_contents, mimetype = attachment # For a mimetype starting with text/, content is expected to be a string. if isinstance(binary_contents, str): binary_contents = binary_contents.encode() contents = base64.b64encode(binary_contents).decode('ascii') message_dict['attachments'].append((filename, contents, mimetype)) if settings.CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES: for attr in settings.CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES: if hasattr(message, attr): message_dict[attr] = getattr(message, attr) return message_dict def dict_to_email(messagedict): message_kwargs = copy.deepcopy(messagedict) # prevents missing items on retry # remove items from message_kwargs until only valid EmailMessage/EmailMultiAlternatives kwargs are left # and save the removed items to be used as EmailMessage/EmailMultiAlternatives attributes later message_attributes = ['content_subtype', 'mixed_subtype'] if settings.CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES: message_attributes.extend(settings.CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES) attributes_to_copy = {} for attr in message_attributes: if attr in message_kwargs: attributes_to_copy[attr] = message_kwargs.pop(attr) # remove attachments from message_kwargs then reinsert after base64 decoding attachments = message_kwargs.pop('attachments') message_kwargs['attachments'] = [] for attachment in attachments: filename, contents, mimetype = attachment contents = base64.b64decode(contents.encode('ascii')) # For a mimetype starting with text/, content is expected to be a string. if mimetype and mimetype.startswith('text/'): contents = contents.decode() message_kwargs['attachments'].append((filename, contents, mimetype)) if 'alternatives' in message_kwargs: message = EmailMultiAlternatives(**message_kwargs) else: message = EmailMessage(**message_kwargs) # set attributes on message with items removed from message_kwargs earlier for attr, val in attributes_to_copy.items(): setattr(message, attr, val) return message django-celery-email-3.0.0/djcelery_email/models.py 0000664 0003720 0003720 00000000000 13574205644 022771 0 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/djcelery_email/tasks.py 0000664 0003720 0003720 00000004673 13574205644 022657 0 ustar travis travis 0000000 0000000 from django.conf import settings from django.core.mail import EmailMessage, get_connection from celery import shared_task # Make sure our AppConf is loaded properly. import djcelery_email.conf # noqa from djcelery_email.utils import dict_to_email, email_to_dict # Messages *must* be dicts, not instances of the EmailMessage class # This is because we expect Celery to use JSON encoding, and we want to prevent # code assuming otherwise. TASK_CONFIG = {'name': 'djcelery_email_send_multiple', 'ignore_result': True} TASK_CONFIG.update(settings.CELERY_EMAIL_TASK_CONFIG) # import base if string to allow a base celery task if 'base' in TASK_CONFIG and isinstance(TASK_CONFIG['base'], str): from django.utils.module_loading import import_string TASK_CONFIG['base'] = import_string(TASK_CONFIG['base']) @shared_task(**TASK_CONFIG) def send_emails(messages, backend_kwargs=None, **kwargs): # backward compat: handle **kwargs and missing backend_kwargs combined_kwargs = {} if backend_kwargs is not None: combined_kwargs.update(backend_kwargs) combined_kwargs.update(kwargs) # backward compat: catch single object or dict if isinstance(messages, (EmailMessage, dict)): messages = [messages] # make sure they're all dicts messages = [email_to_dict(m) for m in messages] conn = get_connection(backend=settings.CELERY_EMAIL_BACKEND, **combined_kwargs) try: conn.open() except Exception: logger.exception("Cannot reach CELERY_EMAIL_BACKEND %s", settings.CELERY_EMAIL_BACKEND) messages_sent = 0 for message in messages: try: sent = conn.send_messages([dict_to_email(message)]) if sent is not None: messages_sent += sent logger.debug("Successfully sent email message to %r.", message['to']) except Exception as e: # Not expecting any specific kind of exception here because it # could be any number of things, depending on the backend logger.warning("Failed to send email message to %r, retrying. (%r)", message['to'], e) send_emails.retry([[message], combined_kwargs], exc=e, throw=False) conn.close() return messages_sent # backwards compatibility SendEmailTask = send_email = send_emails try: from celery.utils.log import get_task_logger logger = get_task_logger(__name__) except ImportError: logger = send_emails.get_logger() django-celery-email-3.0.0/djcelery_email/__init__.py 0000664 0003720 0003720 00000000041 13574205644 023252 0 ustar travis travis 0000000 0000000 from .__about__ import * # noqa django-celery-email-3.0.0/djcelery_email/__about__.py 0000664 0003720 0003720 00000000707 13574205644 023432 0 ustar travis travis 0000000 0000000 __all__ = [ '__title__', '__summary__', '__uri__', '__version__', '__author__', '__email__', '__license__', '__copyright__', ] __title__ = 'django-celery-email' __summary__ = 'An async Django email backend using celery' __uri__ = 'https://github.com/pmclanahan/django-celery-email' __version__ = '3.0.0' __author__ = 'Paul McLanahan' __email__ = 'paul@mclanahan.net' __license__ = 'BSD' __copyright__ = 'Copyright 2015 {0}'.format(__author__) django-celery-email-3.0.0/LICENSE 0000664 0003720 0003720 00000002753 13574205644 017212 0 ustar travis travis 0000000 0000000 Copyright (c) 2010, Paul McLanahan All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Paul McLanahan nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-celery-email-3.0.0/README.rst 0000664 0003720 0003720 00000016502 13574205644 017671 0 ustar travis travis 0000000 0000000 ========================================================== django-celery-email - A Celery-backed Django Email Backend ========================================================== .. image:: https://img.shields.io/travis/pmclanahan/django-celery-email/master.svg :target: https://travis-ci.org/pmclanahan/django-celery-email .. image:: https://img.shields.io/pypi/v/django-celery-email.svg :target: https://pypi.python.org/pypi/django-celery-email A `Django`_ email backend that uses a `Celery`_ queue for out-of-band sending of the messages. .. _`Celery`: http://celeryproject.org/ .. _`Django`: http://www.djangoproject.org/ .. warning:: This version requires the following versions: * Python >= 3.5 * Django 2.2, and 3.0 * Celery 4.0 Using django-celery-email ========================= To enable ``django-celery-email`` for your project you need to add ``djcelery_email`` to ``INSTALLED_APPS``:: INSTALLED_APPS += ("djcelery_email",) You must then set ``django-celery-email`` as your ``EMAIL_BACKEND``:: EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' By default ``django-celery-email`` will use Django's builtin ``SMTP`` email backend for the actual sending of the mail. If you'd like to use another backend, you may set it in ``CELERY_EMAIL_BACKEND`` just like you would normally have set ``EMAIL_BACKEND`` before you were using Celery. In fact, the normal installation procedure will most likely be to get your email working using only Django, then change ``EMAIL_BACKEND`` to ``CELERY_EMAIL_BACKEND``, and then add the new ``EMAIL_BACKEND`` setting from above. Mass email are sent in chunks of size ``CELERY_EMAIL_CHUNK_SIZE`` (defaults to 10). If you need to set any of the settings (attributes) you'd normally be able to set on a `Celery Task`_ class had you written it yourself, you may specify them in a ``dict`` in the ``CELERY_EMAIL_TASK_CONFIG`` setting:: CELERY_EMAIL_TASK_CONFIG = { 'queue' : 'email', 'rate_limit' : '50/m', # * CELERY_EMAIL_CHUNK_SIZE (default: 10) ... } There are some default settings. Unless you specify otherwise, the equivalent of the following settings will apply:: CELERY_EMAIL_TASK_CONFIG = { 'name': 'djcelery_email_send', 'ignore_result': True, } After this setup is complete, and you have a working Celery install, sending email will work exactly like it did before, except that the sending will be handled by your Celery workers:: from django.core import mail emails = ( ('Hey Man', "I'm The Dude! So that's what you call me.", 'dude@aol.com', ['mr@lebowski.com']), ('Dammit Walter', "Let's go bowlin'.", 'dude@aol.com', ['wsobchak@vfw.org']), ) results = mail.send_mass_mail(emails) ``results`` will be a list of celery `AsyncResult`_ objects that you may ignore, or use to check the status of the email delivery task, or even wait for it to complete if want. You have to enable a result backend and set ``ignore_result`` to ``False`` in ``CELERY_EMAIL_TASK_CONFIG`` if you want to use these. You should also set ``CELERY_EMAIL_CHUNK_SIZE = 1`` in settings if you are concerned about task status and results. See the `Celery docs`_ for more info. ``len(results)`` will be the number of emails you attempted to send divided by CELERY_EMAIL_CHUNK_SIZE, and is in no way a reflection on the success or failure of their delivery. .. _`Celery Task`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#basics .. _`Celery docs`: http://celery.readthedocs.org/en/latest/userguide/tasks.html#task-states .. _`AsyncResult`: http://celery.readthedocs.org/en/latest/reference/celery.result.html#celery.result.AsyncResult Changelog ========= 3.0.0 - 2019.12.10 ------------------ * Support for Django 3.0 * Support for Python 3.8 * Droppped support for Django 1.x, Django 2.0 and Django 2.1 * Droppped support for Python 2.7 2.0.2 - 2019.05.29 ------------------ * Reduce memory usage by running email_to_dict on chunks. Thanks `Paul Brown`_. * Simplify dict_to_email for readability and efficiency. Thanks `Paul Brown`_. * Update test matrix for supported versions of Django, Celery and Python. Thanks `James`_. .. _Paul Brown: https://github.com/pawl .. _James: https://github.com/jmsmkn 2.0.1 - 2018.18.27 ------------------ * Fix bug preventing sending text/* encoded mime attachments. Thanks `Cesar Canassa`_. .. _Cesar Canassa: https://github.com/canassa 2.0 - 2017.07.10 ---------------- * Support for Django 1.11 and Celery 4.0 * Dropped support for Celery 2.x and 3.x * Dropped support for Python 3.3 1.1.5 - 2016.07.20 ------------------ * Support extra email attributes via CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES setting * Updated version requirements in README 1.1.4 - 2016.01.19 ------------------ * Support sending email with embedded images. Thanks `Georg Zimmer`_. * Document CELERY_EMAIL_CHUNK_SIZE. Thanks `Jonas Haag`_. * Add exception handling to email backend connection. Thanks `Tom`_. .. _Georg Zimmer: https://github.com/georgmzimmer .. _Tom: https://github.com/tomleo 1.1.3 - 2015.11.06 ------------------ * Support setting celery.base from string. Thanks `Matthew Jacobi`_. * Use six for py2/3 string compatibility. Thanks `Matthew Jacobi`_. * Pass content_subtype back in for retries. Thanks `Mark Joshua Tan`_. * Rework how tests work, add tox, rework travis-ci matrix. * Use six from django.utils. * Release a universal wheel. .. _Matthew Jacobi: https://github.com/oppianmatt .. _Mark Joshua Tan: https://github.com/mark-tan 1.1.2 - 2015.07.06 ------------------ * Fix for HTML-only emails. Thanks `gnarvaja`_. .. _gnarvaja: https://github.com/gnarvaja 1.1.1 - 2015.03.20 ------------------ * Fix for backward compatibility of task kwarg handling - Thanks `Jeremy Thurgood`_. .. _Jeremy Thurgood: https://github.com/jerith 1.1.0 - 2015.03.06 ------------------ * New PyPI release rolling up 1.0.5 changes and some cleanup. * More backward compatability in task. Will still accept message objects and lists of message objects. * Thanks again to everyone who contributed to 1.0.5. 1.0.5 - 2014.08.24 ------------------ * Django 1.6 support, Travis CI testing, chunked sending & more - thanks `Jonas Haag`_. * HTML email support - thanks `Andres Riancho`_. * Support for JSON transit for Celery, sponsored by `DigiACTive`_. * Drop support for Django 1.2. .. _`Jonas Haag`: https://github.com/jonashaag .. _`Andres Riancho`: https://github.com/andresriancho .. _`DigiACTive`: https://github.com/digiactive 1.0.4 - 2013.10.12 ------------------ * Add Django 1.5.2 and Python 3 support. * Thanks to `Stefan Wehrmeyer`_ for the contribution. .. _`Stefan Wehrmeyer`: https://github.com/stefanw 1.0.3 - 2012.03.06 ------------------ * Backend will now pass any kwargs with which it is initialized to the email sending backend. * Thanks to `Fedor Tyurin`_ for the contribution. .. _`Fedor Tyurin`: https://bitbucket.org/ftyurin 1.0.2 - 2012.02.21 ------------------ * Task and backend now accept kwargs that can be used in signal handlers. * Task now returns the result from the email sending backend. * Thanks to `Yehonatan Daniv`_ for these changes. .. _`Yehonatan Daniv`: https://bitbucket.org/ydaniv 1.0.1 - 2011.10.06 ------------------ * Fixed a bug that resulted in tasks that were throwing errors reporting success. * If there is an exception thrown by the sending email backend, the result of the task will now be this exception. django-celery-email-3.0.0/setup.py 0000664 0003720 0003720 00000003141 13574205644 017707 0 ustar travis travis 0000000 0000000 #!/usr/bin/env python import os import codecs from setuptools import setup, find_packages base_dir = os.path.dirname(__file__) with codecs.open(os.path.join(base_dir, 'README.rst'), 'r', encoding='utf8') as f: long_description = f.read() about = {} with open(os.path.join(base_dir, 'djcelery_email', '__about__.py')) as f: exec(f.read(), about) setup( name=about['__title__'], version=about['__version__'], description=about['__summary__'], long_description=long_description, license=about['__license__'], url=about['__uri__'], author=about['__author__'], author_email=about['__email__'], platforms=['any'], packages=find_packages(exclude=['ez_setup', 'tests']), scripts=[], zip_safe=False, install_requires=[ 'django>=2.2', 'celery>=4.0', 'django-appconf', ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Django', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: POSIX', 'Topic :: Communications', 'Topic :: Communications :: Email', 'Topic :: System :: Distributed Computing', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) django-celery-email-3.0.0/tests/ 0000775 0003720 0003720 00000000000 13574205771 017341 5 ustar travis travis 0000000 0000000 django-celery-email-3.0.0/tests/tests.py 0000664 0003720 0003720 00000037170 13574205644 021064 0 ustar travis travis 0000000 0000000 import json import os.path from email.mime.image import MIMEImage from django.core import mail from django.core.mail.backends.base import BaseEmailBackend from django.core.mail.backends import locmem from django.core.mail import EmailMultiAlternatives from django.test import TestCase from django.test.utils import override_settings import celery from djcelery_email import tasks from djcelery_email.utils import email_to_dict, dict_to_email def even(n): return n % 2 == 0 def celery_queue_pop(): """ Pops a single task from Celery's 'memory://' queue. """ with celery.current_app.connection() as conn: queue = conn.SimpleQueue('django_email', no_ack=True) return queue.get().payload class TracingBackend(BaseEmailBackend): def __init__(self, **kwargs): self.__class__.kwargs = kwargs def send_messages(self, messages): self.__class__.called = True class UtilTests(TestCase): @override_settings(CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES=['extra_attribute']) def test_email_to_dict_extra_attrs(self): msg = mail.EmailMessage() msg.extra_attribute = {'name': 'val'} self.assertEqual(email_to_dict(msg)['extra_attribute'], msg.extra_attribute) @override_settings(CELERY_EMAIL_MESSAGE_EXTRA_ATTRIBUTES=['extra_attribute']) def test_dict_to_email_extra_attrs(self): msg_dict = email_to_dict(mail.EmailMessage()) msg_dict['extra_attribute'] = {'name': 'val'} self.assertEqual(email_to_dict(dict_to_email(msg_dict)), msg_dict) def check_json_of_msg(self, msg): serialized = json.dumps(email_to_dict(msg)) self.assertEqual( email_to_dict(dict_to_email(json.loads(serialized))), email_to_dict(msg)) def test_email_with_attachment(self): file_path = os.path.join(os.path.dirname(__file__), 'image.png') with open(file_path, 'rb') as file: file_contents = file.read() msg = mail.EmailMessage( 'test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) msg.attach('image.png', file_contents) self.check_json_of_msg(msg) def test_email_with_mime_attachment(self): file_path = os.path.join(os.path.dirname(__file__), 'image.png') with open(file_path, 'rb') as file: file_contents = file.read() mimg = MIMEImage(file_contents) msg = mail.EmailMessage( 'test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) msg.attach(mimg) self.check_json_of_msg(msg) def test_email_with_attachment_from_file(self): file_path = os.path.join(os.path.dirname(__file__), 'image.png') msg = mail.EmailMessage( 'test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) msg.attach_file(file_path) self.check_json_of_msg(msg) class TaskTests(TestCase): """ Tests that the 'tasks.send_email(s)' task works correctly: - should accept a single or multiple messages (as dicts) - should send all these messages - should use the backend set in CELERY_EMAIL_BACKEND - should pass the given kwargs to that backend - should retry sending failed messages (see TaskErrorTests) """ def test_send_single_email_object(self): """ It should accept and send a single EmailMessage object. """ msg = mail.EmailMessage() tasks.send_email(msg, backend_kwargs={}) self.assertEqual(len(mail.outbox), 1) # we can't compare them directly as it's converted into a dict # for JSONification and then back. Compare dicts instead. self.assertEqual(email_to_dict(msg), email_to_dict(mail.outbox[0])) def test_send_single_email_object_no_backend_kwargs(self): """ It should send email with backend_kwargs not provided. """ msg = mail.EmailMessage() tasks.send_email(msg) self.assertEqual(len(mail.outbox), 1) # we can't compare them directly as it's converted into a dict # for JSONification and then back. Compare dicts instead. self.assertEqual(email_to_dict(msg), email_to_dict(mail.outbox[0])) def test_send_single_email_object_response(self): """ It should return the number of messages sent, 1 here. """ msg = mail.EmailMessage() messages_sent = tasks.send_email(msg) self.assertEqual(messages_sent, 1) self.assertEqual(len(mail.outbox), 1) def test_send_single_email_dict(self): """ It should accept and send a single EmailMessage dict. """ msg = mail.EmailMessage() tasks.send_email(email_to_dict(msg), backend_kwargs={}) self.assertEqual(len(mail.outbox), 1) # we can't compare them directly as it's converted into a dict # for JSONification and then back. Compare dicts instead. self.assertEqual(email_to_dict(msg), email_to_dict(mail.outbox[0])) def test_send_multiple_email_objects(self): """ It should accept and send a list of EmailMessage objects. """ N = 10 msgs = [mail.EmailMessage() for i in range(N)] tasks.send_emails([email_to_dict(msg) for msg in msgs], backend_kwargs={}) self.assertEqual(len(mail.outbox), N) for i in range(N): self.assertEqual(email_to_dict(msgs[i]), email_to_dict(mail.outbox[i])) def test_send_multiple_email_dicts(self): """ It should accept and send a list of EmailMessage dicts. """ N = 10 msgs = [mail.EmailMessage() for i in range(N)] tasks.send_emails(msgs, backend_kwargs={}) self.assertEqual(len(mail.outbox), N) for i in range(N): self.assertEqual(email_to_dict(msgs[i]), email_to_dict(mail.outbox[i])) def test_send_multiple_email_dicts_response(self): """ It should return the number of messages sent. """ N = 10 msgs = [mail.EmailMessage() for i in range(N)] messages_sent = tasks.send_emails(msgs, backend_kwargs={}) self.assertEqual(messages_sent, N) self.assertEqual(len(mail.outbox), N) @override_settings(CELERY_EMAIL_BACKEND='tests.tests.TracingBackend') def test_uses_correct_backend(self): """ It should use the backend configured in CELERY_EMAIL_BACKEND. """ TracingBackend.called = False msg = mail.EmailMessage() tasks.send_email(email_to_dict(msg), backend_kwargs={}) self.assertTrue(TracingBackend.called) @override_settings(CELERY_EMAIL_BACKEND='tests.tests.TracingBackend') def test_backend_parameters(self): """ It should pass kwargs like username and password to the backend. """ TracingBackend.kwargs = None msg = mail.EmailMessage() tasks.send_email(email_to_dict(msg), backend_kwargs={'foo': 'bar'}) self.assertEqual(TracingBackend.kwargs.get('foo'), 'bar') @override_settings(CELERY_EMAIL_BACKEND='tests.tests.TracingBackend') def test_backend_parameters_kwargs(self): """ It should pass on kwargs specified as keyword params. """ TracingBackend.kwargs = None msg = mail.EmailMessage() tasks.send_email(email_to_dict(msg), foo='bar') self.assertEqual(TracingBackend.kwargs.get('foo'), 'bar') class EvenErrorBackend(locmem.EmailBackend): """ Fails to deliver every 2nd message. """ def __init__(self, *args, **kwargs): super(EvenErrorBackend, self).__init__(*args, **kwargs) self.message_count = 0 def send_messages(self, messages): self.message_count += 1 if even(self.message_count-1): raise RuntimeError("Something went wrong sending the message") else: return super(EvenErrorBackend, self).send_messages(messages) class TaskErrorTests(TestCase): """ Tests that the 'tasks.send_emails' task does not crash if a single message could not be sent and that it requeues that message. """ # TODO: replace setUp/tearDown with 'unittest.mock' at some point def setUp(self): super(TaskErrorTests, self).setUp() self._retry_calls = [] def mock_retry(*args, **kwargs): self._retry_calls.append((args, kwargs)) self._old_retry = tasks.send_emails.retry tasks.send_emails.retry = mock_retry def tearDown(self): super(TaskErrorTests, self).tearDown() tasks.send_emails.retry = self._old_retry @override_settings(CELERY_EMAIL_BACKEND='tests.tests.EvenErrorBackend') def test_send_multiple_emails(self): N = 10 msgs = [mail.EmailMessage(subject="msg %d" % i) for i in range(N)] tasks.send_emails([email_to_dict(msg) for msg in msgs], backend_kwargs={'foo': 'bar'}) # Assert that only "odd"/good messages have been sent. self.assertEqual(len(mail.outbox), 5) self.assertEqual( [msg.subject for msg in mail.outbox], ["msg 1", "msg 3", "msg 5", "msg 7", "msg 9"] ) # Assert that "even"/bad messages have been requeued, # one retry task per bad message. self.assertEqual(len(self._retry_calls), 5) odd_msgs = [msg for idx, msg in enumerate(msgs) if even(idx)] for msg, (args, kwargs) in zip(odd_msgs, self._retry_calls): retry_args = args[0] self.assertEqual(retry_args, [[email_to_dict(msg)], {'foo': 'bar'}]) self.assertTrue(isinstance(kwargs.get('exc'), RuntimeError)) self.assertFalse(kwargs.get('throw', True)) class BackendTests(TestCase): """ Tests that our *own* email backend ('backends.CeleryEmailBackend') works, i.e. it submits the correct number of jobs (according to the chunk size) and passes backend parameters to the task. """ # TODO: replace setUp/tearDown with 'unittest.mock' at some point def setUp(self): super(BackendTests, self).setUp() self._delay_calls = [] def mock_delay(*args, **kwargs): self._delay_calls.append((args, kwargs)) self._old_delay = tasks.send_emails.delay tasks.send_emails.delay = mock_delay def tearDown(self): super(BackendTests, self).tearDown() tasks.send_emails.delay = self._old_delay def test_backend_parameters(self): """ Our backend should pass kwargs to the 'send_emails' task. """ kwargs = {'auth_user': 'user', 'auth_password': 'pass'} mail.send_mass_mail([ ('test1', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']), ('test2', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) ], **kwargs) self.assertEqual(len(self._delay_calls), 1) args, kwargs = self._delay_calls[0] messages, backend_kwargs = args self.assertEqual(messages[0]['subject'], 'test1') self.assertEqual(messages[1]['subject'], 'test2') self.assertEqual(backend_kwargs, {'username': 'user', 'password': 'pass'}) def test_chunking(self): """ Given 11 messages and a chunk size of 4, the backend should queue 11/4 = 3 jobs (2 jobs with 4 messages and 1 job with 3 messages). """ N = 11 chunksize = 4 with override_settings(CELERY_EMAIL_CHUNK_SIZE=4): mail.send_mass_mail([ ("subject", "body", "from@example.com", ["to@example.com"]) for _ in range(N) ]) num_chunks = 3 # floor(11.0 / 4.0) self.assertEqual(len(self._delay_calls), num_chunks) full_tasks = self._delay_calls[:-1] last_task = self._delay_calls[-1] for args, kwargs in full_tasks: self.assertEqual(len(args[0]), chunksize) args, kwargs = last_task self.assertEqual(len(args[0]), N % chunksize) class ConfigTests(TestCase): """ Tests that our Celery task has been initialized with the correct options (those set in the CELERY_EMAIL_TASK_CONFIG setting) """ def test_setting_extra_configs(self): self.assertEqual(tasks.send_email.queue, 'django_email') self.assertEqual(tasks.send_email.delivery_mode, 1) self.assertEqual(tasks.send_email.rate_limit, '50/m') class IntegrationTests(TestCase): # We run these tests in ALWAYS_EAGER mode, but they might as well be # executed using a real backend (maybe we can add that to the test setup in # the future?) def setUp(self): super(IntegrationTests, self).setUp() # TODO: replace with 'unittest.mock' at some point celery.current_app.conf.task_always_eager = True def tearDown(self): super(IntegrationTests, self).tearDown() celery.current_app.conf.task_always_eager = False def test_sending_email(self): [result] = mail.send_mail('test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) self.assertEqual(result.get(), 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'test') def test_sending_html_email(self): msg = EmailMultiAlternatives('test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) html = '
Testing with Celery! w00t!!
' msg.attach_alternative(html, 'text/html') [result] = msg.send() self.assertEqual(result.get(), 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'test') self.assertEqual(len(mail.outbox[0].alternatives), 1) self.assertEqual(list(mail.outbox[0].alternatives[0]), [html, 'text/html']) def test_sending_mail_with_text_attachment(self): msg = mail.EmailMessage( 'test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) msg.attach('image.png', 'csv content', 'text/csv') [result] = msg.send() self.assertEqual(result.get(), 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'test') self.assertEqual(mail.outbox[0].content_subtype, "plain") def test_sending_html_only_email(self): msg = mail.EmailMessage('test', 'Testing with Celery! w00t!!', 'from@example.com', ['to@example.com']) msg.content_subtype = "html" [result] = msg.send() self.assertEqual(result.get(), 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'test') self.assertEqual(mail.outbox[0].content_subtype, "html") def test_sending_mass_email(self): emails = ( ('mass 1', 'mass message 1', 'from@example.com', ['to@example.com']), ('mass 2', 'mass message 2', 'from@example.com', ['to@example.com']), ) [result] = mail.send_mass_mail(emails) self.assertEqual(result.get(), 2) self.assertEqual(len(mail.outbox), 2) self.assertEqual(mail.outbox[0].subject, 'mass 1') self.assertEqual(mail.outbox[1].subject, 'mass 2') def test_sending_mass_email_chunked(self): emails = [ ('mass %i' % i, 'message', 'from@example.com', ['to@example.com']) for i in range(11)] with override_settings(CELERY_EMAIL_CHUNK_SIZE=4): [result1, result2, result3] = mail.send_mass_mail(emails) self.assertEqual(result1.get(), 4) self.assertEqual(result2.get(), 4) self.assertEqual(result3.get(), 3) self.assertEqual(len(mail.outbox), 11) self.assertEqual(mail.outbox[0].subject, 'mass 0') self.assertEqual(mail.outbox[1].subject, 'mass 1') django-celery-email-3.0.0/tests/__init__.py 0000664 0003720 0003720 00000000637 13574205644 021457 0 ustar travis travis 0000000 0000000 from django.test.runner import DiscoverRunner class DJCETestSuiteRunner(DiscoverRunner): def setup_test_environment(self, **kwargs): # have to do this here as the default test runner overrides EMAIL_BACKEND super(DJCETestSuiteRunner, self).setup_test_environment(**kwargs) from django.conf import settings settings.EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend' django-celery-email-3.0.0/tests/settings.py 0000664 0003720 0003720 00000001106 13574205644 021550 0 ustar travis travis 0000000 0000000 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } INSTALLED_APPS = ( 'djcelery_email', 'appconf', ) SECRET_KEY = 'unique snowflake' # Django 1.7 throws dire warnings if this is not set. # We don't actually use any middleware, given that there are no views. MIDDLEWARE_CLASSES = () CELERY_EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' CELERY_EMAIL_TASK_CONFIG = { 'queue': 'django_email', 'delivery_mode': 1, # non persistent 'rate_limit': '50/m', # 50 chunks per minute }