Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send mail refactor #14

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 33 additions & 14 deletions django_mailer/__init__.py
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -26,15 +29,31 @@ def send_mail(subject, message, from_email, recipient_list,
arguments are not used.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(the imports are kept locally because setup.py imports the package to get the version number)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed :)

"""
from django.core.mail import EmailMessage
from django.utils.encoding import force_unicode


subject = force_unicode(subject)
email_message = EmailMessage(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
Expand Down Expand Up @@ -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.

Expand All @@ -104,26 +124,25 @@ 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.USE_BACKEND)
result = send_message(email_message, smtp_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(
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,
html_message=html_message)
queued_message = models.QueuedMessage(message=message)
if priority:
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


Expand Down
2 changes: 1 addition & 1 deletion django_mailer/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',)

Expand Down
15 changes: 15 additions & 0 deletions django_mailer/backend.py
Original file line number Diff line number Diff line change
@@ -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
82 changes: 38 additions & 44 deletions django_mailer/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -153,79 +153,73 @@ 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)
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"))
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 = smtp_connection.open()
smtp_connection.connection.sendmail(message.from_address,
[message.to_address],
message.encoded_message)
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 opened_connection:
smtp_connection.close()
if not arg_connection:
connection.close()
return result


def send_message(email_message, smtp_connection=None):
def send_message(message, connection=None):
"""
Send an EmailMessage, returning a response code as to the action taken.

The response codes can be found in ``django_mailer.constants``. The
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())
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):
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)

if opened_connection:
smtp_connection.close()
connection.close()
return result
2 changes: 1 addition & 1 deletion django_mailer/management/commands/send_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 23 additions & 9 deletions django_mailer/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from django.conf import settings
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

import datetime


Expand All @@ -18,19 +22,13 @@

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()
html_message = models.TextField(blank=True)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here...

Firstly, if we're changing the schema then we should probably use south.
Secondly, I'm not sure I like having html_message as a field. It seems like it'd be a better fit to have a MessageAlternative class and actually store it "properly"

date_created = models.DateTimeField(default=datetime.datetime.now)

class Meta:
Expand All @@ -39,6 +37,22 @@ class Meta:
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)
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):
"""
Expand Down
6 changes: 4 additions & 2 deletions django_mailer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
17 changes: 16 additions & 1 deletion django_mailer/smtp_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from django.core.mail.backends.base import BaseEmailBackend

from django_mailer.constants import PRIORITIES, PRIORITY_EMAIL_NOW


class EmailBackend(BaseEmailBackend):
'''
Expand All @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This smells bad. mail.outbox is a list which the tests use, nothing more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Chris,

That's because it's only used during testing (and this is probably a legacy
thing). Now that django has a TestEmailBackend, we probably should be using
that. It doesn't make sense to keep this around either now since we're no
longer limited to SMTP.

Win~

On Tue, Aug 23, 2011 at 12:03 PM, SmileyChris <
[email protected]>wrote:

     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)
    

This smells bad. mail.outbox is a list which the tests use, nothing
more.

Reply to this email directly or view it on GitHub:
https://github.com/SmileyChris/django-mailer-2/pull/14/files#r99679

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Chris,

That's because it's only used during testing (and this is probably a legacy thing). Now that django has a TestEmailBackend, we probably should be using that. It doesn't make sense to keep this around either now since we're no longer limited to SMTP.

else:
queue_email_message(email_message)
num_sent += 1
return num_sent
5 changes: 3 additions & 2 deletions django_mailer/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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
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
Loading