comparison roundup/mailer.py @ 4092:4b0ddce43d08

migrate from MimeWriter to email
author Richard Jones <richard@users.sourceforge.net>
date Thu, 12 Mar 2009 05:55:16 +0000
parents 48457385bf61
children da682f38bad3
comparison
equal deleted inserted replaced
4091:09e79cbeb827 4092:4b0ddce43d08
1 """Sending Roundup-specific mail over SMTP. 1 """Sending Roundup-specific mail over SMTP.
2 """ 2 """
3 __docformat__ = 'restructuredtext' 3 __docformat__ = 'restructuredtext'
4 # $Id: mailer.py,v 1.22 2008-07-21 01:44:58 richard Exp $ 4 # $Id: mailer.py,v 1.22 2008-07-21 01:44:58 richard Exp $
5 5
6 import time, quopri, os, socket, smtplib, re, sys, traceback 6 import time, quopri, os, socket, smtplib, re, sys, traceback, email
7 7
8 from cStringIO import StringIO 8 from cStringIO import StringIO
9 from MimeWriter import MimeWriter 9
10
11 from roundup.rfc2822 import encode_header
12 from roundup import __version__ 10 from roundup import __version__
13 from roundup.date import get_timezone 11 from roundup.date import get_timezone
14 12
15 try: 13 from email.Utils import formatdate, formataddr
16 from email.Utils import formatdate 14 from email.Message import Message
17 except ImportError: 15 from email.Header import Header
18 def formatdate(): 16 from email.MIMEText import MIMEText
19 return time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) 17 from email.MIMEMultipart import MIMEMultipart
20 18
21 class MessageSendError(RuntimeError): 19 class MessageSendError(RuntimeError):
22 pass 20 pass
21
22 def encode_quopri(msg):
23 orig = msg.get_payload()
24 encdata = quopri.encodestring(orig)
25 msg.set_payload(encdata)
26 msg['Content-Transfer-Encoding'] = 'quoted-printable'
23 27
24 class Mailer: 28 class Mailer:
25 """Roundup-specific mail sending.""" 29 """Roundup-specific mail sending."""
26 def __init__(self, config): 30 def __init__(self, config):
27 self.config = config 31 self.config = config
39 # timezone. 43 # timezone.
40 if hasattr(time, 'tzset'): 44 if hasattr(time, 'tzset'):
41 os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None) 45 os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
42 time.tzset() 46 time.tzset()
43 47
44 def get_standard_message(self, to, subject, author=None): 48 def get_standard_message(self, to, subject, author=None, multipart=False):
45 '''Form a standard email message from Roundup. 49 '''Form a standard email message from Roundup.
46 50
47 "to" - recipients list 51 "to" - recipients list
48 "subject" - Subject 52 "subject" - Subject
49 "author" - (name, address) tuple or None for admin email 53 "author" - (name, address) tuple or None for admin email
53 57
54 Returns a Message object and body part writer. 58 Returns a Message object and body part writer.
55 ''' 59 '''
56 # encode header values if they need to be 60 # encode header values if they need to be
57 charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8') 61 charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
58 tracker_name = self.config.TRACKER_NAME 62 tracker_name = unicode(self.config.TRACKER_NAME, 'utf-8')
59 if charset != 'utf-8':
60 tracker = unicode(tracker_name, 'utf-8').encode(charset)
61 if not author: 63 if not author:
62 author = straddr((tracker_name, self.config.ADMIN_EMAIL)) 64 author = formataddr((tracker_name, self.config.ADMIN_EMAIL))
63 else: 65 else:
64 name = author[0] 66 name = unicode(author[0], 'utf-8')
65 if charset != 'utf-8': 67 author = formataddr((name, author[1]))
66 name = unicode(name, 'utf-8').encode(charset) 68
67 author = straddr((encode_header(name, charset), author[1])) 69 if multipart:
68 70 message = MIMEMultipart()
69 message = StringIO() 71 else:
70 writer = MimeWriter(message) 72 message = Message()
71 writer.addheader('Subject', encode_header(subject, charset)) 73 message.set_charset(charset)
72 writer.addheader('To', ', '.join(to)) 74 message['Content-Type'] = 'text/plain; charset="%s"'%charset
73 writer.addheader('From', author) 75
74 writer.addheader('Date', formatdate(localtime=True)) 76 try:
77 message['Subject'] = subject.encode('ascii')
78 except UnicodeError:
79 message['Subject'] = Header(subject, charset)
80 message['To'] = ', '.join(to)
81 try:
82 message['From'] = author.encode('ascii')
83 except UnicodeError:
84 message['From'] = Header(author, charset)
85 message['Date'] = formatdate(localtime=True)
75 86
76 # add a Precedence header so autoresponders ignore us 87 # add a Precedence header so autoresponders ignore us
77 writer.addheader('Precedence', 'bulk') 88 message['Precedence'] = 'bulk'
78 89
79 # Add a unique Roundup header to help filtering 90 # Add a unique Roundup header to help filtering
80 writer.addheader('X-Roundup-Name', encode_header(tracker_name, 91 try:
81 charset)) 92 message['X-Roundup-Name'] = tracker_name.encode('ascii')
93 except UnicodeError:
94 message['X-Roundup-Name'] = Header(tracker_name, charset)
95
82 # and another one to avoid loops 96 # and another one to avoid loops
83 writer.addheader('X-Roundup-Loop', 'hello') 97 message['X-Roundup-Loop'] = 'hello'
84 # finally, an aid to debugging problems 98 # finally, an aid to debugging problems
85 writer.addheader('X-Roundup-Version', __version__) 99 message['X-Roundup-Version'] = __version__
86 100
87 writer.addheader('MIME-Version', '1.0') 101 message['MIME-Version'] = '1.0'
88 102
89 return message, writer 103 return message
90 104
91 def standard_message(self, to, subject, content, author=None): 105 def standard_message(self, to, subject, content, author=None):
92 """Send a standard message. 106 """Send a standard message.
93 107
94 Arguments: 108 Arguments:
95 - to: a list of addresses usable by rfc822.parseaddr(). 109 - to: a list of addresses usable by rfc822.parseaddr().
96 - subject: the subject as a string. 110 - subject: the subject as a string.
97 - content: the body of the message as a string. 111 - content: the body of the message as a string.
98 - author: the sender as a (name, address) tuple 112 - author: the sender as a (name, address) tuple
113
114 All strings are assumed to be UTF-8 encoded.
99 """ 115 """
100 message, writer = self.get_standard_message(to, subject, author) 116 message = self.get_standard_message(to, subject, author)
101 117 message.set_payload(content)
102 writer.addheader('Content-Transfer-Encoding', 'quoted-printable') 118 self.smtp_send(to, str(message))
103 body = writer.startbody('text/plain; charset=utf-8')
104 content = StringIO(content)
105 quopri.encode(content, body, 0)
106
107 self.smtp_send(to, message)
108 119
109 def bounce_message(self, bounced_message, to, error, 120 def bounce_message(self, bounced_message, to, error,
110 subject='Failed issue tracker submission'): 121 subject='Failed issue tracker submission'):
111 """Bounce a message, attaching the failed submission. 122 """Bounce a message, attaching the failed submission.
112 123
126 if error_messages_to == "dispatcher": 137 if error_messages_to == "dispatcher":
127 to = [dispatcher_email] 138 to = [dispatcher_email]
128 elif error_messages_to == "both": 139 elif error_messages_to == "both":
129 to.append(dispatcher_email) 140 to.append(dispatcher_email)
130 141
131 message, writer = self.get_standard_message(to, subject) 142 message = self.get_standard_message(to, subject)
132 143
133 part = writer.startmultipartbody('mixed') 144 # add the error text
134 part = writer.nextpart() 145 part = MIMEText(error)
135 part.addheader('Content-Transfer-Encoding', 'quoted-printable') 146 message.attach(part)
136 body = part.startbody('text/plain; charset=utf-8')
137 body.write(quopri.encodestring ('\n'.join(error)))
138 147
139 # attach the original message to the returned message 148 # attach the original message to the returned message
140 part = writer.nextpart()
141 part.addheader('Content-Disposition', 'attachment')
142 part.addheader('Content-Description', 'Message you sent')
143 body = part.startbody('text/plain')
144
145 for header in bounced_message.headers:
146 body.write(header)
147 body.write('\n')
148 try: 149 try:
149 bounced_message.rewindbody() 150 bounced_message.rewindbody()
150 except IOError, message: 151 except IOError, message:
151 body.write("*** couldn't include message body: %s ***" 152 body.write("*** couldn't include message body: %s ***"
152 % bounced_message) 153 % bounced_message)
153 else: 154 else:
154 body.write(bounced_message.fp.read()) 155 body.write(bounced_message.fp.read())
155 156 part = MIMEText(bounced_message.fp.read())
156 writer.lastpart() 157 part['Content-Disposition'] = 'attachment'
157 158 for header in bounced_message.headers:
158 try: 159 part.write(header)
159 self.smtp_send(to, message) 160 message.attach(part)
161
162 # send
163 try:
164 self.smtp_send(to, str(message))
160 except MessageSendError: 165 except MessageSendError:
161 # squash mail sending errors when bouncing mail 166 # squash mail sending errors when bouncing mail
162 # TODO this *could* be better, as we could notify admin of the 167 # TODO this *could* be better, as we could notify admin of the
163 # problem (even though the vast majority of bounce errors are 168 # problem (even though the vast majority of bounce errors are
164 # because of spam) 169 # because of spam)
182 """ 187 """
183 if self.debug: 188 if self.debug:
184 # don't send - just write to a file 189 # don't send - just write to a file
185 open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' % 190 open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' %
186 (self.config.ADMIN_EMAIL, 191 (self.config.ADMIN_EMAIL,
187 ', '.join(to), 192 ', '.join(to), message))
188 message.getvalue()))
189 else: 193 else:
190 # now try to send the message 194 # now try to send the message
191 try: 195 try:
192 # send the message as admin so bounces are sent there 196 # send the message as admin so bounces are sent there
193 # instead of to roundup 197 # instead of to roundup
194 smtp = SMTPConnection(self.config) 198 smtp = SMTPConnection(self.config)
195 smtp.sendmail(self.config.ADMIN_EMAIL, to, 199 smtp.sendmail(self.config.ADMIN_EMAIL, to, message)
196 message.getvalue())
197 except socket.error, value: 200 except socket.error, value:
198 raise MessageSendError("Error: couldn't send email: " 201 raise MessageSendError("Error: couldn't send email: "
199 "mailhost %s"%value) 202 "mailhost %s"%value)
200 except smtplib.SMTPException, msg: 203 except smtplib.SMTPException, msg:
201 raise MessageSendError("Error: couldn't send email: %s"%msg) 204 raise MessageSendError("Error: couldn't send email: %s"%msg)
215 # ok, now do we also need to log in? 218 # ok, now do we also need to log in?
216 mailuser = config["MAIL_USERNAME"] 219 mailuser = config["MAIL_USERNAME"]
217 if mailuser: 220 if mailuser:
218 self.login(mailuser, config["MAIL_PASSWORD"]) 221 self.login(mailuser, config["MAIL_PASSWORD"])
219 222
220 # use the 'email' module, either imported, or our copied version
221 try :
222 from email.Utils import formataddr as straddr
223 except ImportError :
224 # code taken from the email package 2.4.3
225 def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'),
226 escapesre = re.compile(r'[][\()"]')):
227 name, address = pair
228 if name:
229 quotes = ''
230 if specialsre.search(name):
231 quotes = '"'
232 name = escapesre.sub(r'\\\g<0>', name)
233 return '%s%s%s <%s>' % (quotes, name, quotes, address)
234 return address
235
236 # vim: set et sts=4 sw=4 : 223 # vim: set et sts=4 sw=4 :

Roundup Issue Tracker: http://roundup-tracker.org/