diff roundup/mailgw.py @ 25:4cf1daf2f2eb

More Grande Splite
author Richard Jones <richard@users.sourceforge.net>
date Sun, 22 Jul 2001 12:01:27 +0000
parents
children c7c14960f413
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/roundup/mailgw.py	Sun Jul 22 12:01:27 2001 +0000
@@ -0,0 +1,267 @@
+'''
+Incoming messages are examined for multiple parts. In a multipart/mixed
+message or part, each subpart is extracted and examined. In a
+multipart/alternative message or part, we look for a text/plain subpart and
+ignore the other parts. The text/plain subparts are assembled to form the
+textual body of the message, to be stored in the file associated with a
+"msg" class node. Any parts of other types are each stored in separate
+files and given "file" class nodes that are linked to the "msg" node. 
+
+The "summary" property on message nodes is taken from the first non-quoting
+section in the message body. The message body is divided into sections by
+blank lines. Sections where the second and all subsequent lines begin with
+a ">" or "|" character are considered "quoting sections". The first line of
+the first non-quoting section becomes the summary of the message. 
+
+All of the addresses in the To: and Cc: headers of the incoming message are
+looked up among the user nodes, and the corresponding users are placed in
+the "recipients" property on the new "msg" node. The address in the From:
+header similarly determines the "author" property of the new "msg"
+node. The default handling for addresses that don't have corresponding
+users is to create new users with no passwords and a username equal to the
+address. (The web interface does not permit logins for users with no
+passwords.) If we prefer to reject mail from outside sources, we can simply
+register an auditor on the "user" class that prevents the creation of user
+nodes with no passwords. 
+
+The subject line of the incoming message is examined to determine whether
+the message is an attempt to create a new item or to discuss an existing
+item. A designator enclosed in square brackets is sought as the first thing
+on the subject line (after skipping any "Fwd:" or "Re:" prefixes). 
+
+If an item designator (class name and id number) is found there, the newly
+created "msg" node is added to the "messages" property for that item, and
+any new "file" nodes are added to the "files" property for the item. 
+
+If just an item class name is found there, we attempt to create a new item
+of that class with its "messages" property initialized to contain the new
+"msg" node and its "files" property initialized to contain any new "file"
+nodes. 
+
+Both cases may trigger detectors (in the first case we are calling the
+set() method to add the message to the item's spool; in the second case we
+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.1 2001-07-22 11:58:35 richard Exp $
+'''
+
+
+import string, re, os, mimetools, StringIO, smtplib, socket, binascii, quopri
+import traceback
+import date
+
+def getPart(fp, boundary):
+    line = ''
+    s = StringIO.StringIO()
+    while 1:
+        line_n = fp.readline()
+        if not line_n:
+            break
+        line = line_n.strip()
+        if line == '--'+boundary+'--':
+            break
+        if line == '--'+boundary:
+            break
+        s.write(line_n)
+    if not s.getvalue().strip():
+        return None
+    return s
+
+subject_re = re.compile(r'(\[?(fwd|re):\s*)*'
+    r'(\[(?P<classname>[^\d]+)(?P<nodeid>\d+)?\])'
+    r'(?P<title>[^\[]+)(\[(?P<args>.+?)\])?', re.I)
+
+class MailGW:
+    def __init__(self, db):
+        self.db = db
+
+    def main(self, fp):
+        # ok, figure the subject, author, recipients and content-type
+        message = mimetools.Message(fp)
+        try:
+            self.handle_message(message)
+        except:
+            # bounce the message back to the sender with the error message
+            sendto = [message.getaddrlist('from')[0][1]]
+            m = ['Subject: failed issue tracker submission']
+            m.append('')
+            # TODO as attachments?
+            m.append('----  traceback of failure  ----')
+            s = StringIO.StringIO()
+            import traceback
+            traceback.print_exc(None, s)
+            m.append(s.getvalue())
+            m.append('---- failed message follows ----')
+            try:
+                fp.seek(0)
+            except:
+                pass
+            m.append(fp.read())
+            try:
+                smtp = smtplib.SMTP(self.MAILHOST)
+                smtp.sendmail(self.ADMIN_EMAIL, sendto, '\n'.join(m))
+            except socket.error, value:
+                return "Couldn't send confirmation email: mailhost %s"%value
+            except smtplib.SMTPException, value:
+                return "Couldn't send confirmation email: %s"%value
+
+    def handle_message(self, message):
+        # handle the subject line
+        m = subject_re.match(message.getheader('subject'))
+        if not m:
+            raise ValueError, 'No [designator] found in subject "%s"'
+        classname = m.group('classname')
+        nodeid = m.group('nodeid')
+        title = m.group('title').strip()
+        subject_args = m.group('args')
+        cl = self.db.getclass(classname)
+        properties = cl.getprops()
+        props = {}
+        args = m.group('args')
+        if args:
+            for prop in string.split(m.group('args'), ';'):
+                try:
+                    key, value = prop.split('=')
+                except ValueError, message:
+                    raise ValueError, 'Args list not of form [arg=value,value,...;arg=value,value,value..]  (specific exception message was "%s")'%message
+                type =  properties[key]
+                if type.isStringType:
+                    props[key] = value 
+                elif type.isDateType:
+                    props[key] = date.Date(value)
+                elif type.isIntervalType:
+                    props[key] = date.Interval(value)
+                elif type.isLinkType:
+                    props[key] = value
+                elif type.isMultilinkType:
+                    props[key] = value.split(',')
+
+        # handle the users
+        author = self.db.uidFromAddress(message.getaddrlist('from')[0])
+        recipients = []
+        for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
+            if recipient[1].strip().lower() == self.ISSUE_TRACKER_EMAIL:
+                continue
+            recipients.append(self.db.uidFromAddress(recipient))
+
+        # now handle the body - find the message
+        content_type =  message.gettype()
+        attachments = []
+        if content_type == 'multipart/mixed':
+            boundary = message.getparam('boundary')
+            # skip over the intro to the first boundary
+            part = getPart(message.fp, boundary)
+            content = None
+            while 1:
+                # get the next part
+                part = getPart(message.fp, boundary)
+                if part is None:
+                    break
+                # parse it
+                part.seek(0)
+                submessage = mimetools.Message(part)
+                subtype = submessage.gettype()
+                if subtype == 'text/plain' and not content:
+                    # this one's our content
+                    content = part.read()
+                elif subtype == 'message/rfc822':
+                    i = part.tell()
+                    subsubmess = mimetools.Message(part)
+                    name = subsubmess.getheader('subject')
+                    part.seek(i)
+                    attachments.append((name, 'message/rfc822', part.read()))
+                else:
+                    # try name on Content-Type
+                    name = submessage.getparam('name')
+                    # this is just an attachment
+                    data = part.read()
+                    encoding = submessage.getencoding()
+                    if encoding == 'base64':
+                        data = binascii.a2b_base64(data)
+                    elif encoding == 'quoted-printable':
+                        data = quopri.decode(data)
+                    elif encoding == 'uuencoded':
+                        data = binascii.a2b_uu(data)
+                    attachments.append((name, submessage.gettype(), data))
+            if content is None:
+                raise ValueError, 'No text/plain part found'
+
+        elif content_type[:10] == 'multipart/':
+            boundary = message.getparam('boundary')
+            # skip over the intro to the first boundary
+            getPart(message.fp, boundary)
+            content = None
+            while 1:
+                # get the next part
+                part = getPart(message.fp, boundary)
+                if part is None:
+                    break
+                # parse it
+                part.seek(0)
+                submessage = mimetools.Message(part)
+                if submessage.gettype() == 'text/plain' and not content:
+                    # this one's our content
+                    content = part.read()
+            if content is None:
+                raise ValueError, 'No text/plain part found'
+
+        elif content_type != 'text/plain':
+            raise ValueError, 'No text/plain part found'
+
+        else:
+            content = message.fp.read()
+
+        # extract out the summary from the message
+        summary = []
+        for line in content.split('\n'):
+            line = line.strip()
+            if summary and not line:
+                break
+            if not line:
+                summary.append('')
+            elif line[0] not in '>|':
+                summary.append(line)
+        summary = '\n'.join(summary)
+
+        # handle the files
+        files = []
+        for (name, type, data) in attachments:
+            files.append(self.db.file.create(type=type, name=name, content=data))
+
+        # now handle the db stuff
+        if nodeid:
+            # If an item designator (class name and id number) is found there, the
+            # newly created "msg" node is added to the "messages" property for
+            # that item, and any new "file" nodes are added to the "files" 
+            # property for the item. 
+            message_id = self.db.msg.create(author=author, recipients=recipients,
+                date=date.Date('.'), summary=summary, content=content,
+                files=files)
+            messages = cl.get(nodeid, 'messages')
+            messages.append(message_id)
+            props['messages'] = messages
+            apply(cl.set, (nodeid, ), props)
+        else:
+            # If just an item class name is found there, we attempt to create a
+            # new item of that class with its "messages" property initialized to
+            # contain the new "msg" node and its "files" property initialized to
+            # contain any new "file" nodes. 
+            message_id = self.db.msg.create(author=author, recipients=recipients,
+                date=date.Date('.'), summary=summary, content=content,
+                files=files)
+            if not props.has_key('assignedto'):
+                props['assignedto'] = 1             # "admin"
+            if not props.has_key('priority'):
+                props['priority'] = 1               # "bug-fatal"
+            if not props.has_key('status'):
+                props['status'] = 1                 # "unread"
+            if not props.has_key('title'):
+                props['title'] = title
+            props['messages'] = [message_id]
+            props['nosy'] = recipients[:]
+            props['nosy'].append(author)
+            props['nosy'].sort()
+            nodeid = cl.create(**props)
+

Roundup Issue Tracker: http://roundup-tracker.org/