Mercurial > p > roundup > code
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 : |
