Code

Extracted duplicated mail-sending code from mailgw, roundupdb and client.py to
authorjlgijsbers <jlgijsbers@57a73879-2fb5-44c3-a270-3262357dd7e2>
Mon, 8 Sep 2003 09:28:28 +0000 (09:28 +0000)
committerjlgijsbers <jlgijsbers@57a73879-2fb5-44c3-a270-3262357dd7e2>
Mon, 8 Sep 2003 09:28:28 +0000 (09:28 +0000)
the new mailer.py module.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1866 57a73879-2fb5-44c3-a270-3262357dd7e2

roundup/cgi/client.py
roundup/mailer.py [new file with mode: 0644]
roundup/mailgw.py
roundup/roundupdb.py

index d4228e1780de94b9a1d87fc7f9c362d625d9c1a8..ea1ad7359af8048f831407d3afced4486f1c06e5 100644 (file)
@@ -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 (file)
index 0000000..fcadeab
--- /dev/null
@@ -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
index a07fafde2750bfe61fe9e269e16cb25d9a135400..f6822aa0a33e4eb75e5cbfa37a6d1e04f3f39a0c 100644 (file)
@@ -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
index 14e0622d491b9a80918d3fe9c069422220198615..5bc4928a7af2e94a67591345661b6602e37d9a35 100644 (file)
 # 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