From 00916e28f40b445d4bd441c9c23d4dc7dad0093f Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Sat, 30 Jul 2011 11:09:51 +0700 Subject: [PATCH 1/7] Emails are now sent using EmailMessage.send_message Conflicts: django_mailer/__init__.py --- django_mailer/__init__.py | 7 ++- django_mailer/admin.py | 2 +- django_mailer/backend.py | 15 +++++++ django_mailer/engine.py | 44 +++++++++---------- .../management/commands/send_mail.py | 2 +- django_mailer/models.py | 20 +++++---- django_mailer/settings.py | 6 ++- django_mailer/smtp_queue.py | 17 ++++++- django_mailer/tests/__init__.py | 5 ++- django_mailer/tests/base.py | 19 +------- django_mailer/tests/commands.py | 12 ++++- django_mailer/tests/engine.py | 39 +++++++++++++++- django_mailer/tests/models.py | 20 +++++++++ 13 files changed, 144 insertions(+), 64 deletions(-) create mode 100644 django_mailer/backend.py create mode 100644 django_mailer/tests/models.py diff --git a/django_mailer/__init__.py b/django_mailer/__init__.py index 60046c22..47c14e50 100644 --- a/django_mailer/__init__.py +++ b/django_mailer/__init__.py @@ -108,8 +108,8 @@ def queue_email_message(email_message, fail_silently=False, priority=None): if constants.EMAIL_BACKEND_SUPPORT: from django.core.mail import get_connection from django_mailer.engine import send_message - connection = get_connection(backend=settings.USE_BACKEND) - result = send_message(email_message, smtp_connection=connection) + connection = get_connection(backend=settings.MAILER_BACKEND) + result = send_message(email_message, connection=connection) return (result == constants.RESULT_SENT) else: return email_message.send() @@ -117,8 +117,7 @@ def queue_email_message(email_message, fail_silently=False, priority=None): for to_email in email_message.recipients(): message = models.Message.objects.create( to_address=to_email, from_address=email_message.from_email, - subject=email_message.subject, - encoded_message=email_message.message().as_string()) + subject=email_message.subject, message=email_message.body) queued_message = models.QueuedMessage(message=message) if priority: queued_message.priority = priority diff --git a/django_mailer/admin.py b/django_mailer/admin.py index ecbbe552..6364effe 100644 --- a/django_mailer/admin.py +++ b/django_mailer/admin.py @@ -5,7 +5,7 @@ class Message(admin.ModelAdmin): list_display = ('to_address', 'subject', 'date_created') list_filter = ('date_created',) - search_fields = ('to_address', 'subject', 'from_address', 'encoded_message',) + search_fields = ('to_address', 'subject', 'from_address', 'message',) date_hierarchy = 'date_created' ordering = ('-date_created',) diff --git a/django_mailer/backend.py b/django_mailer/backend.py new file mode 100644 index 00000000..e84b7f6c --- /dev/null +++ b/django_mailer/backend.py @@ -0,0 +1,15 @@ +from django.core.mail.backends.base import BaseEmailBackend + +from mailer.models import Message + + +class DbBackend(BaseEmailBackend): + + def send_messages(self, email_messages): + num_sent = 0 + for email in email_messages: + msg = Message() + msg.email = email + msg.save() + num_sent += 1 + return num_sent diff --git a/django_mailer/engine.py b/django_mailer/engine.py index c4b4ef0b..5792c269 100644 --- a/django_mailer/engine.py +++ b/django_mailer/engine.py @@ -86,7 +86,7 @@ def send_all(block_size=500, backend=None): blacklist = models.Blacklist.objects.values_list('email', flat=True) connection.open() for message in _message_queue(block_size): - result = send_queued_message(message, smtp_connection=connection, + result = send_queued_message(message, connection=connection, blacklist=blacklist) if result == constants.RESULT_SENT: sent += 1 @@ -128,7 +128,7 @@ def send_loop(empty_queue_sleep=None): send_all() -def send_queued_message(queued_message, smtp_connection=None, blacklist=None, +def send_queued_message(queued_message, connection=None, blacklist=None, log=True): """ Send a queued message, returning a response code as to the action taken. @@ -138,9 +138,9 @@ def send_queued_message(queued_message, smtp_connection=None, blacklist=None, ``RESULT_FAILED`` for a deferred message or ``RESULT_SENT`` for a successful sent message. - To allow optimizations if multiple messages are to be sent, an SMTP + To allow optimizations if multiple messages are to be sent, a connection can be provided and a list of blacklisted email addresses. - Otherwise an SMTP connection will be opened to send this message and the + Otherwise a new connection will be opened to send this message and the email recipient address checked against the ``Blacklist`` table. If the message recipient is blacklisted, the message will be removed from @@ -153,9 +153,12 @@ def send_queued_message(queued_message, smtp_connection=None, blacklist=None, """ message = queued_message.message - if smtp_connection is None: - smtp_connection = get_connection() - opened_connection = False + if connection is None: + connection = get_connection() + connection.open() + arg_connection = False + else: + arg_connection = True if blacklist is None: blacklisted = models.Blacklist.objects.filter(email=message.to_address) @@ -173,10 +176,8 @@ def send_queued_message(queued_message, smtp_connection=None, blacklist=None, logger.info("Sending message to %s: %s" % (message.to_address.encode("utf-8"), message.subject.encode("utf-8"))) - opened_connection = smtp_connection.open() - smtp_connection.connection.sendmail(message.from_address, - [message.to_address], - message.encoded_message) + #opened_connection = connection.open() + message.email_message(connection=connection).send() queued_message.delete() result = constants.RESULT_SENT except (SocketError, smtplib.SMTPSenderRefused, @@ -191,12 +192,12 @@ def send_queued_message(queued_message, smtp_connection=None, blacklist=None, models.Log.objects.create(message=message, result=result, log_message=log_message) - if opened_connection: - smtp_connection.close() + if not arg_connection: + connection.close() return result -def send_message(email_message, smtp_connection=None): +def send_message(email_message, connection=None): """ Send an EmailMessage, returning a response code as to the action taken. @@ -204,22 +205,19 @@ def send_message(email_message, smtp_connection=None): response will be either ``RESULT_FAILED`` for a failed send or ``RESULT_SENT`` for a successfully sent message. - To allow optimizations if multiple messages are to be sent, an SMTP - connection can be provided. Otherwise an SMTP connection will be opened + To allow optimizations if multiple messages are to be sent, a + connection can be provided. Otherwise a new connection will be opened to send this message. This function does not perform any logging or queueing. """ - if smtp_connection is None: - smtp_connection = get_connection() + if connection is None: + connection = get_connection() opened_connection = False try: - opened_connection = smtp_connection.open() - smtp_connection.connection.sendmail(email_message.from_email, - email_message.recipients(), - email_message.message().as_string()) + email_message.send() result = constants.RESULT_SENT except (SocketError, smtplib.SMTPSenderRefused, smtplib.SMTPRecipientsRefused, @@ -227,5 +225,5 @@ def send_message(email_message, smtp_connection=None): result = constants.RESULT_FAILED if opened_connection: - smtp_connection.close() + connection.close() return result diff --git a/django_mailer/management/commands/send_mail.py b/django_mailer/management/commands/send_mail.py index d609360a..cdb771ca 100644 --- a/django_mailer/management/commands/send_mail.py +++ b/django_mailer/management/commands/send_mail.py @@ -44,7 +44,7 @@ def handle_noargs(self, verbosity, block_size, count, **options): # if PAUSE_SEND is turned on don't do anything. if not settings.PAUSE_SEND: if EMAIL_BACKEND_SUPPORT: - send_all(block_size, backend=settings.USE_BACKEND) + send_all(block_size, backend=settings.MAILER_BACKEND) else: send_all(block_size) else: diff --git a/django_mailer/models.py b/django_mailer/models.py index b50897db..3ae54bd7 100644 --- a/django_mailer/models.py +++ b/django_mailer/models.py @@ -1,5 +1,9 @@ +from django.conf import settings +from django.core.mail import EmailMessage from django.db import models from django_mailer import constants, managers +from django.utils.encoding import force_unicode + import datetime @@ -18,19 +22,12 @@ class Message(models.Model): """ - An email message. - - The ``to_address``, ``from_address`` and ``subject`` fields are merely for - easy of access for these common values. The ``encoded_message`` field - contains the entire encoded email message ready to be sent to an SMTP - connection. - + A model to hold email information. """ to_address = models.CharField(max_length=200) from_address = models.CharField(max_length=200) subject = models.CharField(max_length=255) - - encoded_message = models.TextField() + message = models.TextField() date_created = models.DateTimeField(default=datetime.datetime.now) class Meta: @@ -39,6 +36,11 @@ class Meta: def __unicode__(self): return '%s: %s' % (self.to_address, self.subject) + def email_message(self, connection=None): + subject = force_unicode(self.subject) + return EmailMessage(subject, self.message, self.from_address, + [self.to_address], connection=connection) + class QueuedMessage(models.Model): """ diff --git a/django_mailer/settings.py b/django_mailer/settings.py index 36e99ce0..bbb57bc0 100644 --- a/django_mailer/settings.py +++ b/django_mailer/settings.py @@ -4,8 +4,10 @@ # Provide a way of temporarily pausing the sending of mail. PAUSE_SEND = getattr(settings, "MAILER_PAUSE_SEND", False) -USE_BACKEND = getattr(settings, 'MAILER_USE_BACKEND', - 'django.core.mail.backends.smtp.EmailBackend') +if hasattr(settings, 'MAILER_USE_BACKEND'): + MAILER_BACKEND = getattr(settings, 'MAILER_USE_BACKEND') +else: + MAILER_BACKEND = getattr(settings, 'EMAIL_BACKEND') # Default priorities for the mail_admins and mail_managers methods. MAIL_ADMINS_PRIORITY = getattr(settings, 'MAILER_MAIL_ADMINS_PRIORITY', diff --git a/django_mailer/smtp_queue.py b/django_mailer/smtp_queue.py index 6d66bf62..bb6b1b60 100644 --- a/django_mailer/smtp_queue.py +++ b/django_mailer/smtp_queue.py @@ -2,6 +2,8 @@ from django.core.mail.backends.base import BaseEmailBackend +from django_mailer.constants import PRIORITIES, PRIORITY_EMAIL_NOW + class EmailBackend(BaseEmailBackend): ''' @@ -27,7 +29,20 @@ def send_messages(self, email_messages): from django_mailer import queue_email_message num_sent = 0 + + ''' + Now that email sending actually calls backend's "send" method, + this had to be tweaked to simply append to outbox when priority + is "now". Passing email to queue_email_message with "now" priority + will call this method again, causing infinite loop. + ''' for email_message in email_messages: - queue_email_message(email_message) + priority = email_message.extra_headers.get('X-Mail-Queue-Priority', + None) + if priority and PRIORITIES[priority] is PRIORITY_EMAIL_NOW: + from django.core import mail + mail.outbox.append(email_message) + else: + queue_email_message(email_message) num_sent += 1 return num_sent diff --git a/django_mailer/tests/__init__.py b/django_mailer/tests/__init__.py index 14768536..20bd2c0b 100644 --- a/django_mailer/tests/__init__.py +++ b/django_mailer/tests/__init__.py @@ -1,3 +1,4 @@ from django_mailer.tests.commands import TestCommands -from django_mailer.tests.engine import LockTest #COULD DROP THIS TEST -from django_mailer.tests.backend import TestBackend \ No newline at end of file +from django_mailer.tests.engine import EngineTest, LockTest #COULD DROP THIS TEST +from django_mailer.tests.backend import TestBackend +from django_mailer.tests.models import MailerModelTest diff --git a/django_mailer/tests/base.py b/django_mailer/tests/base.py index 21faa239..f30b4102 100644 --- a/django_mailer/tests/base.py +++ b/django_mailer/tests/base.py @@ -46,23 +46,6 @@ class MailerTestCase(TestCase): buffer and provides some helper methods. """ - def setUp(self): - if EMAIL_BACKEND_SUPPORT: - self.saved_email_backend = backends.smtp.EmailBackend - backends.smtp.EmailBackend = TestEmailBackend - else: - connection = mail.SMTPConnection - if hasattr(connection, 'connection'): - connection.pretest_connection = connection.connection - connection.connection = FakeConnection() - - def tearDown(self): - if EMAIL_BACKEND_SUPPORT: - backends.smtp.EmailBackend = self.saved_email_backend - else: - connection = mail.SMTPConnection - if hasattr(connection, 'pretest_connection'): - connection.connection = connection.pretest_connection def queue_message(self, subject='test', message='a test message', from_email='sender@djangomailer', @@ -70,4 +53,4 @@ def queue_message(self, subject='test', message='a test message', priority=None): email_message = mail.EmailMessage(subject, message, from_email, recipient_list) - return queue_email_message(email_message, priority=priority) + return queue_email_message(email_message, priority=priority) \ No newline at end of file diff --git a/django_mailer/tests/commands.py b/django_mailer/tests/commands.py index 8c708654..7ccd4f10 100644 --- a/django_mailer/tests/commands.py +++ b/django_mailer/tests/commands.py @@ -1,6 +1,8 @@ +from django.conf import settings as django_settings from django.core import mail from django.core.management import call_command -from django_mailer import models + +from django_mailer import models, settings from django_mailer.tests.base import MailerTestCase import datetime @@ -10,6 +12,12 @@ class TestCommands(MailerTestCase): A test case for management commands provided by django-mailer. """ + + def setUp(self): + super(TestCommands, self).setUp() + settings.MAILER_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + + def test_send_mail(self): """ The ``send_mail`` command initiates the sending of messages in the @@ -63,4 +71,4 @@ def test_retry_deferred(self): .update(deferred=datetime.datetime.now(), retries=4) self.assertEqual(non_deferred_messages.count(), 1) call_command('retry_deferred', verbosity='0', max_retries=3) - self.assertEqual(non_deferred_messages.count(), 3) + self.assertEqual(non_deferred_messages.count(), 3) \ No newline at end of file diff --git a/django_mailer/tests/engine.py b/django_mailer/tests/engine.py index 994637e3..5d7d0d2f 100644 --- a/django_mailer/tests/engine.py +++ b/django_mailer/tests/engine.py @@ -1,6 +1,10 @@ +from django.core import mail +from django.conf import settings as django_settings from django.test import TestCase -from django_mailer import engine, settings +from django_mailer import engine, settings, send_mail +from django_mailer.models import QueuedMessage from django_mailer.lockfile import FileLock + from StringIO import StringIO import logging import time @@ -82,3 +86,36 @@ def fake_time(): 'Lock already in place. Exiting.') finally: time.time = original_time + + +class EngineTest(TestCase): + + + def setUp(self): + self.old_backend = django_settings.EMAIL_BACKEND + django_settings.EMAIL_BACKEND = \ + 'django.core.mail.backends.locmem.EmailBackend' + from django.core import mail + self.mail = mail + self.connection = self.mail.get_connection() + + def tearDown(self): + super(EngineTest, self).tearDown() + django_settings.EMAIL_BACKEND = self.old_backend + + def test_sending_email_uses_opened_connection(self): + """ + Test that send_queued_message command uses the connection that gets + passed in as an argument. Connection stored in self is an instance of + locmem email backend. If we override the email backend with a dummy backend + but passed in the previously opened connection from locmem backend, + we should still get the proper result since send_queued_message uses + the connection we passed in. + """ + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + django_settings.EMAIL_BACKEND = \ + 'django.core.mail.backends.dummy.EmailBackend' + engine.send_queued_message(queued_message, self.connection) + self.assertEqual(len(self.mail.outbox), 1) + \ No newline at end of file diff --git a/django_mailer/tests/models.py b/django_mailer/tests/models.py new file mode 100644 index 00000000..17bdc01a --- /dev/null +++ b/django_mailer/tests/models.py @@ -0,0 +1,20 @@ +from django.core.mail import EmailMessage +from django.test import TestCase + +from django_mailer.models import Message + +class MailerModelTest(TestCase): + + def setUp(self): + pass + + def test_email_message(self): + """ + Test to make sure that Message model's "email_message" method + returns a proper django EmailMessage instance + """ + message = Message.objects.create(to_address='to@example.com', + from_address='from@example.com', subject='Subject', + message='Message') + self.assertEqual(isinstance(message.email_message(), EmailMessage), + True) \ No newline at end of file From 27b63f5b655fbabe4d2eb96d6ff183c81e2942ef Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Fri, 8 Jul 2011 16:21:19 +0700 Subject: [PATCH 2/7] Added more tests for send_queued_message() and blacklist --- django_mailer/tests/engine.py | 55 +++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/django_mailer/tests/engine.py b/django_mailer/tests/engine.py index 5d7d0d2f..7deac676 100644 --- a/django_mailer/tests/engine.py +++ b/django_mailer/tests/engine.py @@ -2,7 +2,8 @@ from django.conf import settings as django_settings from django.test import TestCase from django_mailer import engine, settings, send_mail -from django_mailer.models import QueuedMessage +from django_mailer.engine import send_queued_message +from django_mailer.models import QueuedMessage, Blacklist from django_mailer.lockfile import FileLock from StringIO import StringIO @@ -103,6 +104,39 @@ def tearDown(self): super(EngineTest, self).tearDown() django_settings.EMAIL_BACKEND = self.old_backend + def test_send_queued_message(self): + """ + Ensure that send_queued_message properly delivers email, regardless + of whether connection is passed in. + """ + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + send_queued_message(queued_message, self.connection) + self.assertEqual(len(self.mail.outbox), 1) + + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + send_queued_message(queued_message) + self.assertEqual(len(self.mail.outbox), 2) + + + def test_blacklist(self): + """ + Test that blacklist works properly + """ + Blacklist.objects.create(email='foo@bar.com') + send_mail('Subject', 'Body', 'from@example.com', ['foo@bar.com']) + queued_message = QueuedMessage.objects.latest('id') + send_queued_message(queued_message) + self.assertEqual(len(self.mail.outbox), 0) + + # Explicitly passing in list of blacklisted addresses should also work + send_mail('Subject', 'Body', 'from@example.com', ['bar@foo.com']) + queued_message = QueuedMessage.objects.latest('id') + send_queued_message(queued_message, blacklist=['bar@foo.com']) + self.assertEqual(len(self.mail.outbox), 0) + + def test_sending_email_uses_opened_connection(self): """ Test that send_queued_message command uses the connection that gets @@ -112,10 +146,21 @@ def test_sending_email_uses_opened_connection(self): we should still get the proper result since send_queued_message uses the connection we passed in. """ - send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) - queued_message = QueuedMessage.objects.latest('id') django_settings.EMAIL_BACKEND = \ 'django.core.mail.backends.dummy.EmailBackend' - engine.send_queued_message(queued_message, self.connection) + # Outbox should be empty because send_queued_message uses dummy backend + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + engine.send_queued_message(queued_message) + self.assertEqual(len(self.mail.outbox), 0) + + # Outbox should be populated because send_queued_message uses + # the connection we passed in (locmem) + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + engine.send_queued_message(queued_message, self.connection) self.assertEqual(len(self.mail.outbox), 1) - \ No newline at end of file + + + + From f5e6530caeb711ced9f8a698e4fb459a50cef0e3 Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Fri, 8 Jul 2011 17:13:35 +0700 Subject: [PATCH 3/7] Added html email support --- django_mailer/__init__.py | 29 +++++++++++++++--- django_mailer/models.py | 18 ++++++++++-- django_mailer/tests/engine.py | 14 ++++++++- django_mailer/tests/models.py | 55 +++++++++++++++++++++++++++++++---- 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/django_mailer/__init__.py b/django_mailer/__init__.py index 47c14e50..91724d21 100644 --- a/django_mailer/__init__.py +++ b/django_mailer/__init__.py @@ -1,5 +1,8 @@ import logging +from django.core.mail import EmailMessage, EmailMultiAlternatives +from django.utils.encoding import force_unicode + VERSION = (1, 1, 0, "alpha") logger = logging.getLogger('django_mailer') @@ -26,8 +29,7 @@ def send_mail(subject, message, from_email, recipient_list, arguments are not used. """ - from django.core.mail import EmailMessage - from django.utils.encoding import force_unicode + subject = force_unicode(subject) email_message = EmailMessage(subject, message, from_email, @@ -35,6 +37,23 @@ def send_mail(subject, message, from_email, recipient_list, queue_email_message(email_message, priority=priority) +def send_html_mail(subject, message, html_message, from_email, recipient_list, + fail_silently=False, auth_user=None, auth_password=None, + priority=None): + """ + Add a new html email to the mail queue. This is largely the same as the + ``send_mail`` method above, the only difference being that it passes an + `EmailMultiAlternatives`` instance instead of ``EmailMessage`` to + ``queue_email_message`` + """ + subject = force_unicode(subject) + email_message = EmailMultiAlternatives(subject, message, from_email, + recipient_list) + email_message.attach_alternative(html_message, "text/html") + queue_email_message(email_message, priority=priority, + html_message=html_message) + + def mail_admins(subject, message, fail_silently=False, priority=None): """ Add one or more new messages to the mail queue addressed to the site @@ -83,7 +102,8 @@ def mail_managers(subject, message, fail_silently=False, priority=None): send_mail(subject, message, from_email, recipient_list, priority=priority) -def queue_email_message(email_message, fail_silently=False, priority=None): +def queue_email_message(email_message, fail_silently=False, priority=None, + html_message=''): """ Add new messages to the email queue. @@ -117,7 +137,8 @@ def queue_email_message(email_message, fail_silently=False, priority=None): for to_email in email_message.recipients(): message = models.Message.objects.create( to_address=to_email, from_address=email_message.from_email, - subject=email_message.subject, message=email_message.body) + subject=email_message.subject, message=email_message.body, + html_message=html_message) queued_message = models.QueuedMessage(message=message) if priority: queued_message.priority = priority diff --git a/django_mailer/models.py b/django_mailer/models.py index 3ae54bd7..0d4b5a8a 100644 --- a/django_mailer/models.py +++ b/django_mailer/models.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.core.mail import EmailMessage +from django.core.mail import EmailMessage, EmailMultiAlternatives from django.db import models from django_mailer import constants, managers from django.utils.encoding import force_unicode @@ -28,6 +28,7 @@ class Message(models.Model): from_address = models.CharField(max_length=200) subject = models.CharField(max_length=255) message = models.TextField() + html_message = models.TextField(blank=True) date_created = models.DateTimeField(default=datetime.datetime.now) class Meta: @@ -37,9 +38,20 @@ def __unicode__(self): return '%s: %s' % (self.to_address, self.subject) def email_message(self, connection=None): + """ + Returns a django ``EmailMessage`` or ``EmailMultiAlternatives`` object + from a ``Message`` instance, depending on whether html_message is empty. + """ subject = force_unicode(self.subject) - return EmailMessage(subject, self.message, self.from_address, - [self.to_address], connection=connection) + if self.html_message: + msg = EmailMultiAlternatives(subject, self.message, + self.from_address, [self.to_address], + connection=connection) + msg.attach_alternative(self.html_message, "text/html") + return msg + else: + return EmailMessage(subject, self.message, self.from_address, + [self.to_address], connection=connection) class QueuedMessage(models.Model): diff --git a/django_mailer/tests/engine.py b/django_mailer/tests/engine.py index 7deac676..fd628e4c 100644 --- a/django_mailer/tests/engine.py +++ b/django_mailer/tests/engine.py @@ -1,7 +1,7 @@ from django.core import mail from django.conf import settings as django_settings from django.test import TestCase -from django_mailer import engine, settings, send_mail +from django_mailer import engine, settings, send_mail, send_html_mail from django_mailer.engine import send_queued_message from django_mailer.models import QueuedMessage, Blacklist from django_mailer.lockfile import FileLock @@ -118,6 +118,18 @@ def test_send_queued_message(self): queued_message = QueuedMessage.objects.latest('id') send_queued_message(queued_message) self.assertEqual(len(self.mail.outbox), 2) + + send_html_mail('Subject', 'Body', '

HTML

', 'from@example.com', + ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + send_queued_message(queued_message, self.connection) + self.assertEqual(len(self.mail.outbox), 3) + + send_html_mail('Subject', 'Body', '

HTML

', 'from@example.com', + ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + send_queued_message(queued_message) + self.assertEqual(len(self.mail.outbox), 4) def test_blacklist(self): diff --git a/django_mailer/tests/models.py b/django_mailer/tests/models.py index 17bdc01a..975f2da5 100644 --- a/django_mailer/tests/models.py +++ b/django_mailer/tests/models.py @@ -1,6 +1,7 @@ -from django.core.mail import EmailMessage +from django.core.mail import EmailMessage, EmailMultiAlternatives from django.test import TestCase +from django_mailer import send_mail, send_html_mail from django_mailer.models import Message class MailerModelTest(TestCase): @@ -11,10 +12,54 @@ def setUp(self): def test_email_message(self): """ Test to make sure that Message model's "email_message" method - returns a proper django EmailMessage instance + returns a proper django ``EmailMessage`` or `EmailMultiAlternatives`` + instance """ - message = Message.objects.create(to_address='to@example.com', + msg = Message.objects.create(to_address='to@example.com', from_address='from@example.com', subject='Subject', message='Message') - self.assertEqual(isinstance(message.email_message(), EmailMessage), - True) \ No newline at end of file + self.assertEqual(isinstance(msg.email_message(), EmailMessage), True) + + msg = Message.objects.create(to_address='to@example.com', + from_address='from@example.com', subject='Subject', + message='Message', html_message='

HTML

') + self.assertEqual(isinstance(msg.email_message(),EmailMultiAlternatives), + True) + + def test_send_mail(self): + """ + Test to make sure that send_mail creates the right ``Message`` instance + """ + subject = 'Subject' + content = 'Body' + from_address = 'from@example.com' + to_addresses = ['to1@example.com', 'to2@example.com'] + send_mail(subject, content, from_address, to_addresses) + message = Message.objects.get(pk=1) + self.assertEqual(message.subject, subject) + self.assertEqual(message.message, content) + self.assertEqual(message.from_address, from_address) + self.assertEqual(message.to_address, to_addresses[0]) + message = Message.objects.get(pk=2) + self.assertEqual(message.subject, subject) + self.assertEqual(message.message, content) + self.assertEqual(message.from_address, from_address) + self.assertEqual(message.to_address, to_addresses[1]) + + def test_send_html_mail(self): + """ + Test to make sure that send__html_mail creates the right ``Message`` + instance + """ + subject = 'Subject' + content = 'Body' + html_content = '

Body

' + from_address = 'from@example.com' + to_address = ['to1@example.com'] + send_html_mail(subject, content, html_content, from_address, to_address) + message = Message.objects.get(pk=1) + self.assertEqual(message.subject, subject) + self.assertEqual(message.message, content) + self.assertEqual(message.html_message, html_content) + self.assertEqual(message.from_address, from_address) + self.assertEqual(message.to_address, to_address[0]) From a9aa716f120a7aa3947cf4f083901376e7eba9ad Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Sun, 10 Jul 2011 10:21:27 +0700 Subject: [PATCH 4/7] Streamline the treatment of emails having "now" priority with other emails . --- django_mailer/__init__.py | 17 ++++++++--------- django_mailer/engine.py | 36 ++++++++++++++--------------------- django_mailer/tests/models.py | 22 ++++++++++++++++----- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/django_mailer/__init__.py b/django_mailer/__init__.py index 91724d21..bff930c8 100644 --- a/django_mailer/__init__.py +++ b/django_mailer/__init__.py @@ -124,15 +124,6 @@ def queue_email_message(email_message, fail_silently=False, priority=None, priority = email_message.extra_headers.pop(constants.PRIORITY_HEADER) priority = constants.PRIORITIES.get(priority.lower()) - if priority == constants.PRIORITY_EMAIL_NOW: - if constants.EMAIL_BACKEND_SUPPORT: - from django.core.mail import get_connection - from django_mailer.engine import send_message - connection = get_connection(backend=settings.MAILER_BACKEND) - result = send_message(email_message, connection=connection) - return (result == constants.RESULT_SENT) - else: - return email_message.send() count = 0 for to_email in email_message.recipients(): message = models.Message.objects.create( @@ -144,6 +135,14 @@ def queue_email_message(email_message, fail_silently=False, priority=None, queued_message.priority = priority queued_message.save() count += 1 + + if priority == constants.PRIORITY_EMAIL_NOW: + from django.core.mail import get_connection + from django_mailer.engine import send_message + connection = get_connection(backend=settings.MAILER_BACKEND) + result = send_message(message, connection=connection) + return (result == constants.RESULT_SENT) + return count diff --git a/django_mailer/engine.py b/django_mailer/engine.py index 5792c269..ecf35a09 100644 --- a/django_mailer/engine.py +++ b/django_mailer/engine.py @@ -172,32 +172,14 @@ def send_queued_message(queued_message, connection=None, blacklist=None, queued_message.delete() result = constants.RESULT_SKIPPED else: - try: - logger.info("Sending message to %s: %s" % - (message.to_address.encode("utf-8"), - message.subject.encode("utf-8"))) - #opened_connection = connection.open() - message.email_message(connection=connection).send() - queued_message.delete() - result = constants.RESULT_SENT - except (SocketError, smtplib.SMTPSenderRefused, - smtplib.SMTPRecipientsRefused, - smtplib.SMTPAuthenticationError), err: - queued_message.defer() - logger.warning("Message to %s deferred due to failure: %s" % - (message.to_address.encode("utf-8"), err)) - log_message = unicode(err) - result = constants.RESULT_FAILED - if log: - models.Log.objects.create(message=message, result=result, - log_message=log_message) + result = send_message(message, connection=connection) if not arg_connection: connection.close() return result -def send_message(email_message, connection=None): +def send_message(message, connection=None): """ Send an EmailMessage, returning a response code as to the action taken. @@ -217,12 +199,22 @@ def send_message(email_message, connection=None): opened_connection = False try: - email_message.send() + logger.info("Sending message to %s: %s" % + (message.to_address.encode("utf-8"), + message.subject.encode("utf-8"))) + message.email_message(connection=connection).send() + message.queuedmessage.delete() result = constants.RESULT_SENT except (SocketError, smtplib.SMTPSenderRefused, smtplib.SMTPRecipientsRefused, - smtplib.SMTPAuthenticationError): + smtplib.SMTPAuthenticationError), err: + message.queuedmessage.defer() + logger.warning("Message to %s deferred due to failure: %s" % + (message.to_address.encode("utf-8"), err)) + log_message = unicode(err) result = constants.RESULT_FAILED + models.Log.objects.create(message=message, result=result, + log_message=log_message) if opened_connection: connection.close() diff --git a/django_mailer/tests/models.py b/django_mailer/tests/models.py index 975f2da5..fd917638 100644 --- a/django_mailer/tests/models.py +++ b/django_mailer/tests/models.py @@ -1,14 +1,12 @@ +from django.core import mail from django.core.mail import EmailMessage, EmailMultiAlternatives from django.test import TestCase -from django_mailer import send_mail, send_html_mail -from django_mailer.models import Message +from django_mailer import constants, send_mail, send_html_mail +from django_mailer.models import Message, QueuedMessage class MailerModelTest(TestCase): - def setUp(self): - pass - def test_email_message(self): """ Test to make sure that Message model's "email_message" method @@ -45,6 +43,7 @@ def test_send_mail(self): self.assertEqual(message.message, content) self.assertEqual(message.from_address, from_address) self.assertEqual(message.to_address, to_addresses[1]) + def test_send_html_mail(self): """ @@ -63,3 +62,16 @@ def test_send_html_mail(self): self.assertEqual(message.html_message, html_content) self.assertEqual(message.from_address, from_address) self.assertEqual(message.to_address, to_address[0]) + + + def test_send_priority_now(self): + """ + If send_mail is called with priority of "NOW", the message should + get sent right away and the QueuedMessage instance deleted + """ + send_mail('Subject', 'Body', 'foo@bar.com', ['to1@example.com'], + priority=constants.PRIORITY_EMAIL_NOW) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(QueuedMessage.objects.count(), 0) + + \ No newline at end of file From a27b5635758bbeb0b8847732fc990725f9a74e8c Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Sun, 10 Jul 2011 11:04:21 +0700 Subject: [PATCH 5/7] Fixes and tests for handling errors on email delivery --- django_mailer/engine.py | 18 ++++++++----- django_mailer/tests/__init__.py | 2 +- django_mailer/tests/base.py | 31 +++++++++++++---------- django_mailer/tests/engine.py | 45 ++++++++++++++++++++++++++++++--- 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/django_mailer/engine.py b/django_mailer/engine.py index ecf35a09..d2059f0b 100644 --- a/django_mailer/engine.py +++ b/django_mailer/engine.py @@ -165,7 +165,6 @@ def send_queued_message(queued_message, connection=None, blacklist=None, else: blacklisted = message.to_address in blacklist - log_message = '' if blacklisted: logger.info("Not sending to blacklisted email: %s" % message.to_address.encode("utf-8")) @@ -205,16 +204,21 @@ def send_message(message, connection=None): message.email_message(connection=connection).send() message.queuedmessage.delete() result = constants.RESULT_SENT - except (SocketError, smtplib.SMTPSenderRefused, - smtplib.SMTPRecipientsRefused, - smtplib.SMTPAuthenticationError), err: - message.queuedmessage.defer() + log_message = 'Sent' + except Exception, err: + # Defer emails if errors require manual intervention to fix + fatal_errors = (SocketError, smtplib.SMTPSenderRefused, + smtplib.SMTPRecipientsRefused, + smtplib.SMTPAuthenticationError) + if isinstance(err, fatal_errors): + message.queuedmessage.defer() logger.warning("Message to %s deferred due to failure: %s" % (message.to_address.encode("utf-8"), err)) log_message = unicode(err) result = constants.RESULT_FAILED - models.Log.objects.create(message=message, result=result, - log_message=log_message) + + models.Log.objects.create(message=message, result=result, + log_message=log_message) if opened_connection: connection.close() diff --git a/django_mailer/tests/__init__.py b/django_mailer/tests/__init__.py index 20bd2c0b..9f7d9a86 100644 --- a/django_mailer/tests/__init__.py +++ b/django_mailer/tests/__init__.py @@ -1,4 +1,4 @@ from django_mailer.tests.commands import TestCommands -from django_mailer.tests.engine import EngineTest, LockTest #COULD DROP THIS TEST +from django_mailer.tests.engine import EngineTest, ErrorHandlingTest, LockTest #COULD DROP THIS TEST from django_mailer.tests.backend import TestBackend from django_mailer.tests.models import MailerModelTest diff --git a/django_mailer/tests/base.py b/django_mailer/tests/base.py index f30b4102..372da5f4 100644 --- a/django_mailer/tests/base.py +++ b/django_mailer/tests/base.py @@ -1,3 +1,5 @@ +from smtplib import SMTPRecipientsRefused + from django.core import mail from django.test import TestCase from django_mailer import queue_email_message @@ -25,21 +27,24 @@ def sendmail(self, *args, **kwargs): mail.outbox.append(message) -if EMAIL_BACKEND_SUPPORT: - class TestEmailBackend(backends.base.BaseEmailBackend): - ''' - An EmailBackend used in place of the default - django.core.mail.backends.smtp.EmailBackend. - - ''' - def __init__(self, fail_silently=False, **kwargs): - super(TestEmailBackend, self).__init__(fail_silently=fail_silently) - self.connection = FakeConnection() - - def send_messages(self, email_messages): - pass +class RecipientErrorBackend(backends.base.BaseEmailBackend): + ''' + An EmailBackend that always raises an error during sending + to test if django_mailer handles sending error correctly + ''' + def send_messages(self, email_messages): + raise SMTPRecipientsRefused('Fake Error') +class OtherErrorBackend(backends.base.BaseEmailBackend): + ''' + An EmailBackend that always raises an error during sending + to test if django_mailer handles sending error correctly + ''' + def send_messages(self, email_messages): + raise Exception('Fake Error') + + class MailerTestCase(TestCase): """ A base class for Django Mailer test cases which diverts emails to the test diff --git a/django_mailer/tests/engine.py b/django_mailer/tests/engine.py index fd628e4c..43966818 100644 --- a/django_mailer/tests/engine.py +++ b/django_mailer/tests/engine.py @@ -171,8 +171,47 @@ def test_sending_email_uses_opened_connection(self): send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) queued_message = QueuedMessage.objects.latest('id') engine.send_queued_message(queued_message, self.connection) - self.assertEqual(len(self.mail.outbox), 1) - - + self.assertEqual(len(self.mail.outbox), 1) + + +class ErrorHandlingTest(TestCase): + + def setUp(self): + self.old_backend = django_settings.EMAIL_BACKEND + django_settings.EMAIL_BACKEND = \ + 'django_mailer.tests.base.RecipientErrorBackend' + + def tearDown(self): + super(ErrorHandlingTest, self).tearDown() + django_settings.EMAIL_BACKEND = self.old_backend + + def test_queue_not_deleted_on_error(self): + """ + Queued message instance shouldn't be deleted when error is raised + during sending + """ + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + engine.send_queued_message(queued_message) + self.assertEqual(QueuedMessage.objects.count(), 1) + + def test_message_deferred(self): + """ + When error returned requires manual intervention to fix, + emails should be deferred. + """ + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + self.assertEqual(queued_message.deferred, None) + engine.send_queued_message(queued_message) + queued_message = QueuedMessage.objects.latest('id') + self.assertNotEqual(queued_message.deferred, None) + # If we see some other random errors email shouldn't be deferred + django_settings.EMAIL_BACKEND = \ + 'django_mailer.tests.base.OtherErrorBackend' + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + engine.send_queued_message(queued_message) + self.assertEqual(queued_message.deferred, None) From 2b2d8392772e68461c702bfe6c9a44af2e04d070 Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Sun, 10 Jul 2011 11:10:14 +0700 Subject: [PATCH 6/7] Test email logging --- django_mailer/tests/engine.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/django_mailer/tests/engine.py b/django_mailer/tests/engine.py index 43966818..9ea523bf 100644 --- a/django_mailer/tests/engine.py +++ b/django_mailer/tests/engine.py @@ -1,9 +1,10 @@ from django.core import mail from django.conf import settings as django_settings from django.test import TestCase -from django_mailer import engine, settings, send_mail, send_html_mail +from django_mailer import (constants, engine, settings, send_mail, + send_html_mail) from django_mailer.engine import send_queued_message -from django_mailer.models import QueuedMessage, Blacklist +from django_mailer.models import Blacklist, Log, QueuedMessage from django_mailer.lockfile import FileLock from StringIO import StringIO @@ -171,7 +172,20 @@ def test_sending_email_uses_opened_connection(self): send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) queued_message = QueuedMessage.objects.latest('id') engine.send_queued_message(queued_message, self.connection) - self.assertEqual(len(self.mail.outbox), 1) + self.assertEqual(len(self.mail.outbox), 1) + + def test_log(self): + """ + All emails sent through django_mailer should be logged, + even those having "now" priority + """ + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com']) + queued_message = QueuedMessage.objects.latest('id') + engine.send_queued_message(queued_message, self.connection) + self.assertEqual(Log.objects.count(), 1) + send_mail('Subject', 'Body', 'from@example.com', ['to1@example.com'], + priority=constants.PRIORITIES['now']) + self.assertEqual(Log.objects.count(), 2) class ErrorHandlingTest(TestCase): @@ -194,6 +208,7 @@ def test_queue_not_deleted_on_error(self): queued_message = QueuedMessage.objects.latest('id') engine.send_queued_message(queued_message) self.assertEqual(QueuedMessage.objects.count(), 1) + def test_message_deferred(self): """ From 3264ea36bdb894747b875c8ee5412d4f7425d16b Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Sun, 31 Jul 2011 15:46:06 +0700 Subject: [PATCH 7/7] Moved import statements to local scope --- django_mailer/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/django_mailer/__init__.py b/django_mailer/__init__.py index bff930c8..e784701c 100644 --- a/django_mailer/__init__.py +++ b/django_mailer/__init__.py @@ -1,8 +1,5 @@ import logging -from django.core.mail import EmailMessage, EmailMultiAlternatives -from django.utils.encoding import force_unicode - VERSION = (1, 1, 0, "alpha") logger = logging.getLogger('django_mailer') @@ -29,7 +26,8 @@ def send_mail(subject, message, from_email, recipient_list, arguments are not used. """ - + from django.core.mail import EmailMessage + from django.utils.encoding import force_unicode subject = force_unicode(subject) email_message = EmailMessage(subject, message, from_email, @@ -46,6 +44,9 @@ def send_html_mail(subject, message, html_message, from_email, recipient_list, `EmailMultiAlternatives`` instance instead of ``EmailMessage`` to ``queue_email_message`` """ + from django.core.mail import EmailMultiAlternatives + from django.utils.encoding import force_unicode + subject = force_unicode(subject) email_message = EmailMultiAlternatives(subject, message, from_email, recipient_list)