Code

added date header to emails (sf bug 651358)
[roundup.git] / roundup / mailgw.py
index 9f339e7a078bb96ad2ac0f5d114a3ec92a790f01..9de891be5ccd136bbbc6de31f5b9800ab533c9bb 100644 (file)
@@ -16,7 +16,7 @@
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
 
-__doc__ = '''
+'''
 An e-mail gateway for Roundup.
 
 Incoming messages are examined for multiple parts:
@@ -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.89 2002-09-25 02:10:24 richard Exp $
+$Id: mailgw.py,v 1.102 2002-12-11 01:52:20 richard Exp $
 '''
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
@@ -92,6 +92,10 @@ class MailUsageError(ValueError):
 class MailUsageHelp(Exception):
     pass
 
+class MailLoop(Exception):
+    ''' We've seen this message before... '''
+    pass
+
 class Unauthorized(Exception):
     """ Access denied """
 
@@ -130,9 +134,9 @@ class Message(mimetools.Message):
         s.seek(0)
         return Message(s)
 
-subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re|aw)\s*\W?\s*)*'
-    r'\s*(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])?'
-    r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I)
+subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re|aw)\W\s*)*'
+    r'\s*(?P<quote>")?(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])?'
+    r'\s*(?P<title>[^[]+)?"?(\[(?P<args>.+?)\])?', re.I)
 
 class MailGW:
     def __init__(self, instance, db):
@@ -145,8 +149,16 @@ class MailGW:
 
     def do_pipe(self):
         ''' Read a message from standard input and pass it to the mail handler.
+
+            Read into an internal structure that we can seek on (in case
+            there's an error).
+
+            XXX: we may want to read this into a temporary file instead...
         '''
-        self.main(sys.stdin)
+        s = cStringIO.StringIO()
+        s.write(sys.stdin.read())
+        s.seek(0)
+        self.main(s)
         return 0
 
     def do_mailbox(self, filename):
@@ -262,8 +274,12 @@ class MailGW:
                 m = ['']
                 m.append(str(value))
                 m = self.bounce_message(message, sendto, m)
+            except MailLoop:
+                # XXX we should use a log file here...
+                return
             except:
                 # bounce the message back to the sender with the error message
+                # XXX we should use a log file here...
                 sendto = [sendto[0][1], self.instance.config.ADMIN_EMAIL]
                 m = ['']
                 m.append('An unexpected error occurred during the processing')
@@ -277,6 +293,7 @@ class MailGW:
                 m = self.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...
             sendto = [self.instance.config.ADMIN_EMAIL]
             m = ['Subject: badly formed message from mail gateway']
             m.append('')
@@ -311,45 +328,34 @@ class MailGW:
         '''
         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')
         body.write('\n'.join(error))
 
-        # reconstruct the original message
-        m = cStringIO.StringIO()
-        w = MimeWriter.MimeWriter(m)
-        # default the content_type, just in case...
-        content_type = 'text/plain'
-        # add the headers except the content-type
+        # 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:
-            header_name = header.split(':')[0]
-            if header_name.lower() == 'content-type':
-                content_type = message.getheader(header_name)
-            elif message.getheader(header_name):
-                w.addheader(header_name, message.getheader(header_name))
-        # now attach the message body
-        body = w.startbody(content_type)
+            body.write(header)
+        body.write('\n')
         try:
             message.rewindbody()
-        except IOError:
-            body.write("*** couldn't include message body: read from pipe ***")
+        except IOError, message:
+            body.write("*** couldn't include message body: %s ***"%message)
         else:
             body.write(message.fp.read())
 
-        # attach the original message to the returned message
-        part = writer.nextpart()
-        part.addheader('Content-Disposition','attachment')
-        part.addheader('Content-Description','Message you sent')
-        part.addheader('Content-Transfer-Encoding', '7bit')
-        body = part.startbody('message/rfc822')
-        body.write(m.getvalue())
-
         writer.lastpart()
         return msg
 
@@ -377,6 +383,10 @@ class MailGW:
 
         Parse the message as per the module docstring.
         '''
+        # detect loops
+        if message.getheader('x-roundup-loop', ''):
+            raise MailLoop
+
         # handle the subject line
         subject = message.getheader('subject', '')
 
@@ -435,6 +445,11 @@ Subject was: "%s"
         else:
             title = ''
 
+        # strip off the quotes that dumb emailers put around the subject, like
+        #      Re: "[issue1] bla blah"
+        if m.group('quote') and title.endswith('"'):
+            title = title[:-1]
+
         # but we do need either a title or a nodeid...
         if nodeid is None and not title:
             raise MailUsageError, '''
@@ -478,17 +493,24 @@ Subject was: "%s"
         author = uidFromAddress(self.db, message.getaddrlist('from')[0],
             create=create)
 
-        # no author? means we're not author
+        # if we're not recognised, and we don't get added as a user, then we
+        # must be anonymous
         if not author:
-            raise Unauthorized, '''
+            author = anonid
+
+        # make sure the author has permission to use the email interface
+        if not self.db.security.hasPermission('Email Access', author):
+            if author == anonid:
+                # we're anonymous and we need to be a registered user
+                raise Unauthorized, '''
 You are not a registered user.
 
 Unknown address: %s
 '''%message.getaddrlist('from')[0][1]
-
-        # make sure the author has permission to use the email interface
-        if not self.db.security.hasPermission('Email Access', author):
-            raise Unauthorized, 'You are not permitted to access this tracker.'
+            else:
+                # we're registered and we're _still_ not allowed access
+                raise Unauthorized, 'You are not permitted to access '\
+                    'this tracker.'
 
         # make sure they're allowed to edit this class of information
         if not self.db.security.hasPermission('Edit', author, classname):
@@ -717,7 +739,11 @@ Subject was: "%s"
                     attachments.append((name, 'message/rfc822', part.fp.read()))
                 else:
                     # try name on Content-Type
-                    name = part.getparam('name')
+                    name = part.getparam('name').strip()
+                    if not name:
+                        disp = part.getheader('content-disposition', None)
+                        if disp:
+                            name = disp.getparam('filename').strip()
                     # this is just an attachment
                     data = self.get_part_data_decoded(part) 
                     attachments.append((name, part.gettype(), data))
@@ -755,9 +781,9 @@ not find a text/plain part to use.
             content = self.get_part_data_decoded(message) 
  
         # figure how much we should muck around with the email body
-        keep_citations = getattr(self.instance, 'EMAIL_KEEP_QUOTED_TEXT',
+        keep_citations = getattr(self.instance.config, 'EMAIL_KEEP_QUOTED_TEXT',
             'no') == 'yes'
-        keep_body = getattr(self.instance, 'EMAIL_LEAVE_BODY_UNCHANGED',
+        keep_body = getattr(self.instance.config, 'EMAIL_LEAVE_BODY_UNCHANGED',
             'no') == 'yes'
 
         # parse the body of the message, stripping out bits as appropriate
@@ -847,8 +873,7 @@ def uidFromAddress(db, address, create=1):
     # try the user alternate addresses if possible
     props = db.user.getprops()
     if props.has_key('alternate_addresses'):
-        users = db.user.filter(None, {'alternate_addresses': address},
-            [], [])
+        users = db.user.filter(None, {'alternate_addresses': address})
         user = extractUserFromList(db.user, users)
         if user is not None: return user
 
@@ -900,7 +925,7 @@ def parseContent(content, keep_citations, keep_body,
             # see if there's a response somewhere inside this section (ie.
             # no blank line between quoted message and response)
             for line in lines[1:]:
-                if line[0] not in '>|':
+                if line and line[0] not in '>|':
                     break
             else:
                 # we keep quoted bits if specified in the config
@@ -908,17 +933,16 @@ def parseContent(content, keep_citations, keep_body,
                     l.append(section)
                 continue
             # keep this section - it has reponse stuff in it
-            if not summary:
-                # and while we're at it, use the first non-quoted bit as
-                # our summary
-                summary = line
             lines = lines[lines.index(line):]
             section = '\n'.join(lines)
+            # and while we're at it, use the first non-quoted bit as
+            # our summary
+            summary = section
 
         if not summary:
             # if we don't have our summary yet use the first line of this
             # section
-            summary = lines[0]
+            summary = section
         elif signature.match(lines[0]) and 2 <= len(lines) <= 10:
             # lose any signature
             break
@@ -929,6 +953,16 @@ def parseContent(content, keep_citations, keep_body,
         # and add the section to the output
         l.append(section)
 
+    # figure the summary - find the first sentence-ending punctuation or the
+    # first whole line, whichever is longest
+    sentence = re.search(r'^([^!?\.]+[!?\.])', summary)
+    if sentence:
+        sentence = sentence.group(1)
+    else:
+        sentence = ''
+    first = eol.split(summary)[0]
+    summary = max(sentence, first)
+
     # Now reconstitute the message content minus the bits we don't care
     # about.
     if not keep_body: