Code

handle KeyboardInterrupt nicely
[roundup.git] / roundup / mailgw.py
index 9f339e7a078bb96ad2ac0f5d114a3ec92a790f01..8b4f95e6de567160cee5a5f0b85788fb3f455a9e 100644 (file)
@@ -16,7 +16,7 @@
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
 
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
 
-__doc__ = '''
+'''
 An e-mail gateway for Roundup.
 
 Incoming messages are examined for multiple parts:
 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. 
 
 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
 '''
 
 import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
@@ -130,9 +130,9 @@ class Message(mimetools.Message):
         s.seek(0)
         return Message(s)
 
         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):
 
 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.
 
     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):
         return 0
 
     def do_mailbox(self, filename):
@@ -321,35 +329,21 @@ class MailGW:
         body = part.startbody('text/plain')
         body.write('\n'.join(error))
 
         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:
         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()
         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())
 
         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
 
         writer.lastpart()
         return msg
 
@@ -435,6 +429,11 @@ Subject was: "%s"
         else:
             title = ''
 
         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, '''
         # 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)
 
         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:
         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]
 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):
 
         # 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'):
     # 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
 
         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:]:
             # 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
                     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
                     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)
             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
 
         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
         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)
 
         # 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:
     # Now reconstitute the message content minus the bits we don't care
     # about.
     if not keep_body: