Code

handle KeyboardInterrupt nicely
[roundup.git] / roundup / mailgw.py
index 9f339e7a078bb96ad2ac0f5d114a3ec92a790f01..8b4f95e6de567160cee5a5f0b85788fb3f455a9e 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.99 2002-11-05 22:59:46 richard Exp $
 '''
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
@@ -130,9 +130,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 +145,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):
@@ -321,35 +329,21 @@ class MailGW:
         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
 
@@ -435,6 +429,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 +477,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):
@@ -847,8 +853,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 +905,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 +913,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 +933,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: