Login

Email queue in DB

Author:
fish2000
Posted:
April 19, 2010
Language:
Python
Version:
1.1
Score:
2 (after 2 ratings)

This is what I use to send simple status emails from my sites. Instead of a django.core.mail.send_mail call, which can take an irrritatingly, nondeterministically long time to return (regardless of error state), you can stow the emails in the database and rely on a separate interpreter process send them off (using a per-minute cron job or what have you). You then also have a record of everything you've sent via email from your code without having to configure your outbound SMTP server to store them or anything like that.

Usage notes are in the docstring; share and enjoy.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
###################################### YOUR_SITE.YOUR_APP.models.py ######################################

from django.core.mail import send_mail
import smtplib

class QueuedEmail(models.Model):
	"""
	DATABASE EMAIL QUEUE --
	QueuedEmail stashes your sites' messages in a table, so you don't have to wait on
	synchronous calls to django.core.mail.send_mail. Mail stays in the database after
	sending for as long as you leave them there, which provides a paper trail and
	a basis for analytics. I wrote it for use with an invitation-only site, where
	the flushqueuedemail.py script is run with a cron job.
	
	USAGE EXAMPLE --
	This simple bit here will queue a new message from within any python code:
	
	def some_view_function(request):
		(...)
		invitation = FSQueuedEmail(
			toaddress=invite.sentto,
			subject="Welcome",
			body=u'''
			Hello,
		
			Welcome to the site, %s. Your username is %s.
		
			Regards,
			The Management
			''' % (
				request.user.get_full_name(),
				request.user.username,
			)
		)
		invitation.save()
		(...)
	
	You can pass either an instance of django.contrib.auth.models.User in the 'to' param, or use
	a string with an email address in it with 'toaddress'. One can easily integrate templating,
	or compose emails programmatically, or embed the email text for quick development and testing.
	
	Add this model to your site, and flush the queue with the attached command-line tool (or write your
	own, it's like one query and one loop, and hey it could be smaller.)
	
	Enjoy!
	
	-fish2000
	
	"""
	class Meta:
		abstract = False
		verbose_name = "queued email message"
		verbose_name_plural = "queued email messages"
	status = models.IntegerField(verbose_name="Status",
		editable=True,
		null=False,
		default=0,
		choices=(
			(-1, 'SMTP Fail'),
			(0, 'Queued'),
			(1, 'Sent OK'),
			(2, 'Unexpected Error'),
		))
	createdate = models.DateTimeField('Created on',
		default=datetime.now,
		blank=True,
		editable=False)
	modifydate = models.DateTimeField('Last modified on',
		default=datetime.now,
		blank=True,
		editable=False)
	senddate = models.DateTimeField('Send after',
		default=datetime.now,
		blank=True,
		editable=True)
	subject = models.CharField(verbose_name="Subject",
		default="",
		unique=False,
		blank=True,
		max_length=255)
	body = models.TextField(verbose_name="Body",
		default="",
		unique=False,
		blank=True)
	to = models.ForeignKey(User,
		default=None,
		unique=False,
		blank=True,
		null=True,
		verbose_name="To (User)")
	toaddress = models.EmailField(verbose_name="To (Additional address or addresses)",
		default=None,
		unique=False,
		blank=True,
		null=True,
		max_length=255)
	def save(self, force_insert=False, force_update=False):
		self.modifydate = datetime.now()
		super(QueuedEmail, self).save(force_insert, force_update)
	def sendit(self):
		if self.senddate < datetime.now():
			if self.subject and self.body:
				if self.toaddress:
					try:
						self.status = send_mail(
							# one can insert [sitename] or somesuch 
							# at the start of the subject here
							"%s" % self.subject,
							self.body,
							settings.EMAIL_HOST_USER,
							[self.toaddress]
						)
					except smtplib.SMTPException:
						self.status = -1
						self.save()
					else:
						self.save()
				elif self.to and self.to.email:
					try:
						self.status = send_mail(
							"%s" % self.subject, 
							self.body,
							settings.EMAIL_HOST_USER,
							[self.to.email]
						)
					except smtplib.SMTPException:
						self.status = -1
						self.save()
					else:
						self.save()
		return self.status


###################################### flushqueuedemail.py ######################################

#!/usr/bin/env python
# encoding: utf-8

import sys, os, getopt
from django.core.management import setup_environ
import settings
setup_environ(settings)

from django.db.models import Q
from YOUR_SITE.models import QueuedEmail

class Usage(Exception):
	def __init__(self, msg):
		self.msg = msg

def main(argv=None):
	if argv is None:
		argv = sys.argv
	try:
		try:
			opts, args = getopt.getopt(argv[1:], "hv", ["help", "verbose"])
		except getopt.error, msg:
			raise Usage(msg)
		for option, value in opts:
			if option in ("-v", "--verbose"):
				verbose = True
			if option in ("-h", "--help"):
				raise Usage("QueuedEmail queue flush tool. Use -v for verbose.")
	except Usage, err:
		print >> sys.stderr, sys.argv[0].split("/")[-1] + ": " + str(err.msg)
		print >> sys.stderr, "\t for help use --help"
		return 2
	
	mailqueue = FSQueuedEmail.objects.filter(
		Q(status=0) & (Q(to__isnull=False) | Q(toaddress__isnull=False) & Q(subject__isnull=False))
	)
	
	if verbose:
		print "###\t Mail queue contains %s items to be sent.\n" % len(mailqueue)
	
	for qm in mailqueue:
		if verbose:
			print "###\t Sending message..."
			print "---\t TO:\t\t\t %s" % qm.to
			print "---\t TOADDRESS:\t\t %s" % qm.toaddress
			print "---\t SUBJECT:\t\t %s" % qm.subject
		
		# output will be 1 if sending was successful
		# messages will be saved to the DB with the
		# same number for their status.
		if qm.sendit() == 1:
			if verbose:
				print "---\t Mail sent OK\n"
			else:
				print "###\t MAIL FAIL\n"

if __name__ == "__main__":
	sys.exit(main())

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

phxx (on April 20, 2010):

Nice script but I think there is already a reusable app for this purpose. Search for django-mailer which handles the same usecase.

#

Please login first before commenting.