From: jlgijsbers Date: Mon, 8 Sep 2003 09:28:28 +0000 (+0000) Subject: Extracted duplicated mail-sending code from mailgw, roundupdb and client.py to X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=ecc82323a216277dff3f2275991dc4d498945301;p=roundup.git Extracted duplicated mail-sending code from mailgw, roundupdb and client.py to the new mailer.py module. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1866 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index d4228e1..ea1ad73 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -1,4 +1,4 @@ -# $Id: client.py,v 1.135 2003-09-07 22:12:24 richard Exp $ +# $Id: client.py,v 1.136 2003-09-08 09:28:28 jlgijsbers Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -14,7 +14,8 @@ from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate from roundup.cgi import cgitb from roundup.cgi.PageTemplates import PageTemplate from roundup.rfc2822 import encode_header -from roundup.mailgw import uidFromAddress, openSMTPConnection +from roundup.mailgw import uidFromAddress +from roundup.mailer import Mailer, MessageSendError class HTTPException(Exception): pass @@ -27,10 +28,6 @@ class Redirect(HTTPException): class NotModified(HTTPException): pass -# set to indicate to roundup not to actually _send_ email -# this var must contain a file to write the mail to -SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') - # used by a couple of routines if hasattr(string, 'ascii_letters'): chars = string.ascii_letters+string.digits @@ -164,6 +161,7 @@ class Client: self.instance = instance self.request = request self.env = env + self.mailer = Mailer(instance.config) # save off the path self.path = env['PATH_INFO'] @@ -776,7 +774,7 @@ please visit the following URL: %(url)s?@action=confrego&otk=%(otk)s '''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base, 'otk': otk} - if not self.sendEmail(props['address'], subject, body): + if not self.standard_message(props['address'], subject, body): return # commit changes to the database @@ -785,49 +783,13 @@ please visit the following URL: # redirect to the "you're almost there" page raise Redirect, '%suser?@template=rego_progress'%self.base - def sendEmail(self, to, subject, content): - # send email to the user's email address - message = StringIO.StringIO() - writer = MimeWriter.MimeWriter(message) - tracker_name = self.db.config.TRACKER_NAME - writer.addheader('Subject', encode_header(subject)) - writer.addheader('To', to) - writer.addheader('From', roundupdb.straddr((tracker_name, - self.db.config.ADMIN_EMAIL))) - writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", - time.gmtime())) - # add a uniquely Roundup header to help filtering - writer.addheader('X-Roundup-Name', tracker_name) - # avoid email loops - writer.addheader('X-Roundup-Loop', 'hello') - writer.addheader('Content-Transfer-Encoding', 'quoted-printable') - body = writer.startbody('text/plain; charset=utf-8') - - # message body, encoded quoted-printable - content = StringIO.StringIO(content) - quopri.encode(content, body, 0) - - if SENDMAILDEBUG: - # don't send - just write to a file - open(SENDMAILDEBUG, 'a').write('FROM: %s\nTO: %s\n%s\n'%( - self.db.config.ADMIN_EMAIL, - ', '.join(to),message.getvalue())) - else: - # now try to send the message - try: - # send the message as admin so bounces are sent there - # instead of to roundup - smtp = openSMTPConnection(self.db.config) - smtp.sendmail(self.db.config.ADMIN_EMAIL, [to], - message.getvalue()) - except socket.error, value: - self.error_message.append("Error: couldn't send email: " - "mailhost %s"%value) - return 0 - except smtplib.SMTPException, msg: - self.error_message.append("Error: couldn't send email: %s"%msg) - return 0 - return 1 + def standard_message(self, to, subject, body): + try: + self.mailer.standard_message(to, subject, body) + return 1 + except MessageSendException, e: + self.error_message.append(str(e)) + def registerPermission(self, props): ''' Determine whether the user has permission to register @@ -917,7 +879,7 @@ The password has been reset for username "%(name)s". Your password is now: %(password)s '''%{'name': name, 'password': newpw} - if not self.sendEmail(address, subject, body): + if not self.standard_message(address, subject, body): return self.ok_message.append('Password reset and email sent to %s'%address) @@ -960,7 +922,7 @@ the link below: You should then receive another email with the new password. '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk} - if not self.sendEmail(address, subject, body): + if not self.standard_message(address, subject, body): return self.ok_message.append('Email sent to %s'%address) diff --git a/roundup/mailer.py b/roundup/mailer.py new file mode 100644 index 0000000..fcadeab --- /dev/null +++ b/roundup/mailer.py @@ -0,0 +1,148 @@ +"""Sending Roundup-specific mail over SMTP.""" +# $Id: mailer.py,v 1.1 2003-09-08 09:28:28 jlgijsbers Exp $ + +import time, quopri, os, socket, smtplib, re + +from cStringIO import StringIO +from MimeWriter import MimeWriter + +from roundup.rfc2822 import encode_header + +class MessageSendError(RuntimeError): + pass + +class Mailer: + """Roundup-specific mail sending.""" + def __init__(self, config): + self.config = config + + # set to indicate to roundup not to actually _send_ email + # this var must contain a file to write the mail to + self.debug = os.environ.get('SENDMAILDEBUG', '') + + def get_standard_message(self, to, subject, author=None): + if not author: + author = straddr((self.config.TRACKER_NAME, + self.config.ADMIN_EMAIL)) + message = StringIO() + writer = MimeWriter(message) + writer.addheader('Subject', encode_header(subject)) + writer.addheader('To', to) + writer.addheader('From', author) + writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", + time.gmtime())) + + # Add a unique Roundup header to help filtering + writer.addheader('X-Roundup-Name', self.config.TRACKER_NAME) + # and another one to avoid loops + writer.addheader('X-Roundup-Loop', 'hello') + + writer.addheader('MIME-Version', '1.0') + + return message, writer + + def standard_message(self, to, subject, content): + message, writer = self.get_standard_message(to, subject) + + writer.addheader('Content-Transfer-Encoding', 'quoted-printable') + body = writer.startbody('text/plain; charset=utf-8') + content = StringIO(content) + quopri.encode(content, body, 0) + + self.smtp_send(to, message) + + def bounce_message(self, bounced_message, to, error, + subject='Failed issue tracker submission'): + message, writer = self.get_standard_message(', '.join(to), subject) + + part = writer.startmultipartbody('mixed') + part = writer.nextpart() + part.addheader('Content-Transfer-Encoding', 'quoted-printable') + body = part.startbody('text/plain; charset=utf-8') + body.write('\n'.join(error)) + + # attach the original message to the returned message + part = writer.nextpart() + part.addheader('Content-Disposition', 'attachment') + part.addheader('Content-Description', 'Message you sent') + body = part.startbody('text/plain') + + for header in bounced_message.headers: + body.write(header) + body.write('\n') + try: + bounced_message.rewindbody() + except IOError, message: + body.write("*** couldn't include message body: %s ***" + % bounced_message) + else: + body.write(bounced_message.fp.read()) + + writer.lastpart() + + self.smtp_send(to, message) + + def smtp_send(self, to, message): + if self.debug: + # don't send - just write to a file + open(self.debug, 'a').write('FROM: %s\nTO: %s\n%s\n' % + (self.config.ADMIN_EMAIL, + ', '.join(to), + message.getvalue())) + else: + # now try to send the message + try: + # send the message as admin so bounces are sent there + # instead of to roundup + smtp = SMTPConnection(self.config) + smtp.sendmail(self.config.ADMIN_EMAIL, [to], + message.getvalue()) + except socket.error, value: + raise MessageSendError("Error: couldn't send email: " + "mailhost %s"%value) + except smtplib.SMTPException, msg: + raise MessageSendError("Error: couldn't send email: %s"%msg) + +class SMTPConnection(smtplib.SMTP): + ''' Open an SMTP connection to the mailhost specified in the config + ''' + def __init__(self, config): + + smtplib.SMTP.__init__(self, config.MAILHOST) + + # use TLS? + use_tls = getattr(config, 'MAILHOST_TLS', 'no') + if use_tls == 'yes': + # do we have key files too? + keyfile = getattr(config, 'MAILHOST_TLS_KEYFILE', '') + if keyfile: + certfile = getattr(config, 'MAILHOST_TLS_CERTFILE', '') + if certfile: + args = (keyfile, certfile) + else: + args = (keyfile, ) + else: + args = () + # start the TLS + self.starttls(*args) + + # ok, now do we also need to log in? + mailuser = getattr(config, 'MAILUSER', None) + if mailuser: + self.login(*config.MAILUSER) + +# use the 'email' module, either imported, or our copied version +try : + from email.Utils import formataddr as straddr +except ImportError : + # code taken from the email package 2.4.3 + def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'), + escapesre = re.compile(r'[][\()"]')): + name, address = pair + if name: + quotes = '' + if specialsre.search(name): + quotes = '"' + name = escapesre.sub(r'\\\g<0>', name) + return '%s%s%s <%s>' % (quotes, name, quotes, address) + return address diff --git a/roundup/mailgw.py b/roundup/mailgw.py index a07fafd..f6822aa 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -73,7 +73,7 @@ are calling the create() method to create a new node). If an auditor raises an exception, the original message is bounced back to the sender with the explanatory message given in the exception. -$Id: mailgw.py,v 1.129 2003-09-06 10:37:11 jlgijsbers Exp $ +$Id: mailgw.py,v 1.130 2003-09-08 09:28:28 jlgijsbers Exp $ """ import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri @@ -81,6 +81,7 @@ import time, random, sys import traceback, MimeWriter, rfc822 from roundup import hyperdb, date, password, rfc2822 +from roundup.mailer import Mailer SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') @@ -132,35 +133,6 @@ def getparam(str, param): return rfc822.unquote(f[i+1:].strip()) return None -def openSMTPConnection(config): - ''' Open an SMTP connection to the mailhost specified in the config - ''' - smtp = smtplib.SMTP(config.MAILHOST) - - # use TLS? - use_tls = getattr(config, 'MAILHOST_TLS', 'no') - if use_tls == 'yes': - # do we have key files too? - keyfile = getattr(config, 'MAILHOST_TLS_KEYFILE', '') - if keyfile: - certfile = getattr(config, 'MAILHOST_TLS_CERTFILE', '') - if certfile: - args = (keyfile, certfile) - else: - args = (keyfile, ) - else: - args = () - # start the TLS - smtp.starttls(*args) - - # ok, now do we also need to log in? - mailuser = getattr(config, 'MAILUSER', None) - if mailuser: - smtp.login(*config.MAILUSER) - - # that's it, a fully-configured SMTP connection ready to go - return smtp - class Message(mimetools.Message): ''' subclass mimetools.Message so we can retrieve the parts of the message... @@ -209,6 +181,7 @@ class MailGW: self.instance = instance self.db = db self.arguments = arguments + self.mailer = Mailer(instance.config) # should we trap exceptions (normal usage) or pass them through # (for testing) @@ -337,7 +310,7 @@ class MailGW: m = [''] m.append('\n\nMail Gateway Help\n=================') m.append(fulldoc) - m = self.bounce_message(message, sendto, m, + self.mailer.bounce_message(message, sendto, m, subject="Mail Gateway Help") except MailUsageError, value: # bounce the message back to the sender with the usage message @@ -347,13 +320,13 @@ class MailGW: m.append(str(value)) m.append('\n\nMail Gateway Help\n=================') m.append(fulldoc) - m = self.bounce_message(message, sendto, m) + self.mailer.bounce_message(message, sendto, m) except Unauthorized, value: # just inform the user that he is not authorized sendto = [sendto[0][1]] m = [''] m.append(str(value)) - m = self.bounce_message(message, sendto, m) + self.mailer.bounce_message(message, sendto, m) except MailLoop: # XXX we should use a log file here... return @@ -370,7 +343,7 @@ class MailGW: import traceback traceback.print_exc(None, s) m.append(s.getvalue()) - m = self.bounce_message(message, sendto, m) + self.mailer.bounce_message(message, sendto, m) else: # very bad-looking message - we don't even know who sent it # XXX we should use a log file here... @@ -381,64 +354,9 @@ class MailGW: m.append('line, indicating that it is corrupt. Please check your') m.append('mail gateway source. Failed message is attached.') m.append('') - m = self.bounce_message(message, sendto, m, + self.mailer.bounce_message(message, sendto, m, subject='Badly formed message from mail gateway') - # now send the message - if SENDMAILDEBUG: - open(SENDMAILDEBUG, 'a').write('From: %s\nTo: %s\n%s\n'%( - self.instance.config.ADMIN_EMAIL, ', '.join(sendto), - m.getvalue())) - else: - try: - smtp = openSMTPConnection(self.instance.config) - smtp.sendmail(self.instance.config.ADMIN_EMAIL, sendto, - m.getvalue()) - except socket.error, value: - raise MailGWError, "Couldn't send error email: "\ - "mailhost %s"%value - except smtplib.SMTPException, value: - raise MailGWError, "Couldn't send error email: %s"%value - - def bounce_message(self, message, sendto, error, - subject='Failed issue tracker submission'): - ''' create a message that explains the reason for the failed - issue submission to the author and attach the original - message. - ''' - msg = cStringIO.StringIO() - writer = MimeWriter.MimeWriter(msg) - writer.addheader('X-Roundup-Loop', 'hello') - writer.addheader('Subject', subject) - writer.addheader('From', '%s <%s>'% (self.instance.config.TRACKER_NAME, - self.instance.config.TRACKER_EMAIL)) - writer.addheader('To', ','.join(sendto)) - writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", - time.gmtime())) - writer.addheader('MIME-Version', '1.0') - part = writer.startmultipartbody('mixed') - part = writer.nextpart() - body = part.startbody('text/plain; charset=utf-8') - body.write('\n'.join(error)) - - # attach the original message to the returned message - part = writer.nextpart() - part.addheader('Content-Disposition','attachment') - part.addheader('Content-Description','Message you sent') - body = part.startbody('text/plain') - for header in message.headers: - body.write(header) - body.write('\n') - try: - message.rewindbody() - except IOError, message: - body.write("*** couldn't include message body: %s ***"%message) - else: - body.write(message.fp.read()) - - writer.lastpart() - return msg - def get_part_data_decoded(self,part): encoding = part.getencoding() data = None diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 14e0622..5bc4928 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -15,42 +15,21 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundupdb.py,v 1.88 2003-09-06 20:02:23 jlgijsbers Exp $ +# $Id: roundupdb.py,v 1.89 2003-09-08 09:28:28 jlgijsbers Exp $ __doc__ = """ Extending hyperdb with types specific to issue-tracking. """ import re, os, smtplib, socket, time, random -import MimeWriter, cStringIO -import base64, quopri, mimetypes +import cStringIO, base64, quopri, mimetypes from rfc2822 import encode_header -from roundup import password, date - -# if available, use the 'email' module, otherwise fallback to 'rfc822' -try : - from email.Utils import formataddr as straddr -except ImportError : - # code taken from the email package 2.4.3 - def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'), - escapesre = re.compile(r'[][\()"]')): - name, address = pair - if name: - quotes = '' - if specialsre.search(name): - quotes = '"' - name = escapesre.sub(r'\\\g<0>', name) - return '%s%s%s <%s>' % (quotes, name, quotes, address) - return address - -from roundup import hyperdb -from roundup.mailgw import openSMTPConnection - -# set to indicate to roundup not to actually _send_ email -# this var must contain a file to write the mail to -SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') +from roundup import password, date, hyperdb + +# MessageSendError is imported for backwards compatibility +from roundup.mailer import Mailer, straddr, MessageSendError class Database: def getuid(self): @@ -112,12 +91,10 @@ class Database: return userid -class MessageSendError(RuntimeError): - pass class DetectorError(RuntimeError): - ''' Raised by detectors that want to indicate that something's amiss - ''' + """ Raised by detectors that want to indicate that something's amiss + """ pass # deviation from spec - was called IssueClass @@ -311,30 +288,21 @@ class IssueClass: if from_tag: from_tag = ' ' + from_tag + subject = '[%s%s] %s' % (cn, nodeid, encode_header(title)) + author = straddr((encode_header(authname) + from_tag, from_address)) + # create the message - message = cStringIO.StringIO() - writer = MimeWriter.MimeWriter(message) - writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, - encode_header(title))) - writer.addheader('To', ', '.join(sendto)) - writer.addheader('From', straddr((encode_header(authname) + - from_tag, from_address))) + mailer = Mailer(self.db.config) + message, writer = mailer.get_standard_message(', '.join(sendto), + subject, author) + tracker_name = encode_header(self.db.config.TRACKER_NAME) writer.addheader('Reply-To', straddr((tracker_name, from_address))) - writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000", - time.gmtime())) - writer.addheader('MIME-Version', '1.0') if messageid: writer.addheader('Message-Id', messageid) if inreplyto: writer.addheader('In-Reply-To', inreplyto) - # add a uniquely Roundup header to help filtering - writer.addheader('X-Roundup-Name', tracker_name) - - # avoid email loops - writer.addheader('X-Roundup-Loop', 'hello') - # attach files if message_files: part = writer.startmultipartbody('mixed') @@ -371,24 +339,7 @@ class IssueClass: body = writer.startbody('text/plain; charset=utf-8') body.write(content_encoded) - # now try to send the message - if SENDMAILDEBUG: - open(SENDMAILDEBUG, 'a').write('FROM: %s\nTO: %s\n%s\n'%( - self.db.config.ADMIN_EMAIL, - ', '.join(sendto),message.getvalue())) - else: - try: - # send the message as admin so bounces are sent there - # instead of to roundup - smtp = openSMTPConnection(self.db.config) - smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto, - message.getvalue()) - except socket.error, value: - raise MessageSendError, \ - "Couldn't send confirmation email: mailhost %s"%value - except smtplib.SMTPException, value: - raise MessageSendError, \ - "Couldn't send confirmation email: %s"%value + mailer.smtp_send(sendto, message) def email_signature(self, nodeid, msgid): ''' Add a signature to the e-mail with some useful information