Skip to content

Commit

Permalink
Merge pull request #19 from mattrobenolt/better-bulk-sending
Browse files Browse the repository at this point in the history
Better bulk sending, HTTPS, batching, filtering, utility methods for cleaner code.
  • Loading branch information
themartorana committed May 31, 2012
2 parents 52977a7 + 824514b commit b68a1b4
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 99 deletions.
110 changes: 59 additions & 51 deletions postmark/core.py
Expand Up @@ -44,7 +44,7 @@ def default(self, o):

#
#
__POSTMARK_URL__ = 'http://api.postmarkapp.com/'
__POSTMARK_URL__ = 'https://api.postmarkapp.com/'

class PMMail(object):
'''
Expand Down Expand Up @@ -439,9 +439,15 @@ def send(self, test=None):
raise PMMailURLException('URLError: %d: The server couldn\'t fufill the request. (See "inner_exception" for details)' % err.code, err)
else:
raise PMMailURLException('URLError: The server couldn\'t fufill the request. (See "inner_exception" for details)', err)


# Simple utility that returns a generator to chunk up a list into equal parts
def _chunks(l, n):
return (l[i:i+n] for i in range(0, len(l), n))

class PMBatchMail(object):
# Maximum number of messages to be sent at once.
# Ref: http://developer.postmarkapp.com/developer-build.html#batching-messages
MAX_MESSAGES = 500

def __init__(self, **kwargs):
self.__api_key = None
Expand Down Expand Up @@ -473,62 +479,64 @@ def __init__(self, **kwargs):


def send(self, test=None):
json_message = []
for message in self.messages:
json_message.append(message.to_json_message())

req = urllib2.Request(
__POSTMARK_URL__ + 'email/batch',
json.dumps(json_message, cls=PMJSONEncoder),
{
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Postmark-Server-Token': self.__api_key,
'User-agent': self.__user_agent
}
)

# If test is not specified, attempt to read the Django setting
if test is None:
try:
from django.conf import settings as django_settings
test = getattr(django_settings, "POSTMARK_TEST_MODE", None)
except ImportError:
pass

# If this is a test, just print the message
if test:
print 'JSON message is:\n%s' % json.dumps(json_message, cls=PMJSONEncoder)
return

# Attempt send
try:
result = urllib2.urlopen(req)
result.close()
if result.code == 200:
return True
else:
raise PMMailSendException('Return code %d: %s' % (result.code, result.msg))
except urllib2.HTTPError, err:
if err.code == 401:
raise PMMailUnauthorizedException('Sending Unauthorized - incorrect API key.', err)
elif err.code == 422:
try:
jsontxt = err.read()
jsonobj = json.loads(jsontxt)
desc = jsonobj['Message']
except:
desc = 'Description not given'
raise PMMailUnprocessableEntityException('Unprocessable Entity: %s' % desc)
elif err.code == 500:
raise PMMailServerErrorException('Internal server error at Postmark. Admins have been alerted.', err)
except urllib2.URLError, err:
if hasattr(err, 'reason'):
raise PMMailURLException('URLError: Failed to reach the server: %s (See "inner_exception" for details)' % err.reason, err)
elif hasattr(err, 'code'):
raise PMMailURLException('URLError: %d: The server couldn\'t fufill the request. (See "inner_exception" for details)' % err.code, err)
else:
raise PMMailURLException('URLError: The server couldn\'t fufill the request. (See "inner_exception" for details)', err)
# Split up into groups of 500 messages for sending
for messages in _chunks(self.messages, PMBatchMail.MAX_MESSAGES):
json_message = []
for message in messages:
json_message.append(message.to_json_message())

req = urllib2.Request(
__POSTMARK_URL__ + 'email/batch',
json.dumps(json_message, cls=PMJSONEncoder),
{
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Postmark-Server-Token': self.__api_key,
'User-agent': self.__user_agent
}
)

# If this is a test, just print the message
if test:
print 'JSON message is:\n%s' % json.dumps(json_message, cls=PMJSONEncoder)
continue

# Attempt send
try:
result = urllib2.urlopen(req)
result.close()
if result.code == 200:
pass
else:
raise PMMailSendException('Return code %d: %s' % (result.code, result.msg))
except urllib2.HTTPError, err:
if err.code == 401:
raise PMMailUnauthorizedException('Sending Unauthorized - incorrect API key.', err)
elif err.code == 422:
try:
jsontxt = err.read()
jsonobj = json.loads(jsontxt)
desc = jsonobj['Message']
except:
desc = 'Description not given'
raise PMMailUnprocessableEntityException('Unprocessable Entity: %s' % desc)
elif err.code == 500:
raise PMMailServerErrorException('Internal server error at Postmark. Admins have been alerted.', err)
except urllib2.URLError, err:
if hasattr(err, 'reason'):
raise PMMailURLException('URLError: Failed to reach the server: %s (See "inner_exception" for details)' % err.reason, err)
elif hasattr(err, 'code'):
raise PMMailURLException('URLError: %d: The server couldn\'t fufill the request. (See "inner_exception" for details)' % err.code, err)
else:
raise PMMailURLException('URLError: The server couldn\'t fufill the request. (See "inner_exception" for details)', err)
return True



Expand Down
106 changes: 60 additions & 46 deletions postmark/django_backend.py
Expand Up @@ -3,7 +3,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.core.mail import EmailMessage, EmailMultiAlternatives

from core import PMMail
from core import PMMail, PMBatchMail

class PMEmailMessage(EmailMessage):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -45,58 +45,72 @@ def send_messages(self, email_messages):
"""
if not email_messages:
return
num_sent = 0
for message in email_messages:
sent = self._send(message)
if sent:
num_sent += 1
return num_sent
sent = self._send(email_messages)
if sent:
return len(email_messages)
return 0


def _send(self, message):
"""A helper method that does the actual sending."""
def _build_message(self, message):
"""A helper method to convert a PMEmailMessage to a PMMail"""
if not message.recipients():
return False
recipients = ','.join(message.to)
recipients_bcc = ','.join(message.bcc)

html_body = None
if isinstance(message, EmailMultiAlternatives):
for alt in message.alternatives:
if alt[1] == "text/html":
html_body=alt[0]
break

reply_to = None
custom_headers = {}
if message.extra_headers and isinstance(message.extra_headers, dict):
if message.extra_headers.has_key('Reply-To'):
reply_to = message.extra_headers.pop('Reply-To')
if len(message.extra_headers):
custom_headers = message.extra_headers
attachments = []
if message.attachments and isinstance(message.attachments, list):
if len(message.attachments):
attachments = message.attachments

postmark_message = PMMail(api_key=self.api_key,
subject=message.subject,
sender=message.from_email,
to=recipients,
bcc=recipients_bcc,
text_body=message.body,
html_body=html_body,
reply_to=reply_to,
custom_headers=custom_headers,
attachments=attachments)

postmark_message.tag = getattr(message, 'tag', None)
return postmark_message

def _send(self, messages):
"""A helper method that does the actual sending."""
if len(messages) == 1:
to_send = self._build_message(messages[0])
if to_send == False:
# The message was missing recipients.
# Bail.
return False
else:
pm_messages = map(self._build_message, messages)
pm_messages = filter(lambda m: m != False, pm_messages)
if len(pm_messages) == 0:
# If after filtering, there aren't any messages
# to send, bail.
return False
to_send = PMBatchMail(messages=pm_messages)
try:
recipients = ','.join(message.to)
recipients_bcc = ','.join(message.bcc)

html_body = None
if isinstance(message, EmailMultiAlternatives):
for alt in message.alternatives:
if alt[1] == "text/html":
html_body=alt[0]
break

reply_to = None
custom_headers = {}
if message.extra_headers and isinstance(message.extra_headers, dict):
if message.extra_headers.has_key('Reply-To'):
reply_to = message.extra_headers.pop('Reply-To')
if len(message.extra_headers):
custom_headers = message.extra_headers
attachments = []
if message.attachments and isinstance(message.attachments, list):
if len(message.attachments):
attachments = message.attachments

postmark_message = PMMail(api_key=self.api_key,
subject=message.subject,
sender=message.from_email,
to=recipients,
bcc=recipients_bcc,
text_body=message.body,
html_body=html_body,
reply_to=reply_to,
custom_headers=custom_headers,
attachments=attachments)

postmark_message.tag = getattr(message, 'tag', None)

postmark_message.send(test=self.test_mode)
to_send.send(test=self.test_mode)
except:
if self.fail_silently:
return False
raise
return True

7 changes: 5 additions & 2 deletions test/demo/settings.py
Expand Up @@ -2,6 +2,9 @@

import os
settings_path, settings_module = os.path.split(__file__)

import sys
sys.path.append('../../')

DEBUG = True
#TEMPLATE_DEBUG = DEBUG
Expand All @@ -26,5 +29,5 @@
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_BACKEND = 'postmark.django_backend.EmailBackend'
POSTMARK_API_KEY = '1dd7e294-1059-43e8-ac32-d1ce25378223'
POSTMARK_SENDER = 'system@playcompete.com'
POSTMARK_API_KEY = 'POSTMARK_API_TEST'
POSTMARK_SENDER = 'system@playcompete.com'

0 comments on commit b68a1b4

Please sign in to comment.