diff roundup/mailgw.py @ 602:c242455d9b46 config-0-4-0-branch

Brought the config branch up to date with HEAD
author Richard Jones <richard@users.sourceforge.net>
date Wed, 06 Feb 2002 04:05:55 +0000
parents 00450ff9c4e7
children
line wrap: on
line diff
--- a/roundup/mailgw.py	Wed Feb 06 03:47:17 2002 +0000
+++ b/roundup/mailgw.py	Wed Feb 06 04:05:55 2002 +0000
@@ -73,7 +73,7 @@
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception. 
 
-$Id: mailgw.py,v 1.47 2002-01-02 02:32:38 richard Exp $
+$Id: mailgw.py,v 1.47.2.1 2002-02-06 04:05:53 richard Exp $
 '''
 
 
@@ -90,6 +90,9 @@
 class MailUsageError(ValueError):
     pass
 
+class MailUsageHelp(Exception):
+    pass
+
 class UnAuthorized(Exception):
     """ Access denied """
 
@@ -116,7 +119,7 @@
         s.seek(0)
         return Message(s)
 
-subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re)\s*\W?\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)
 
@@ -145,6 +148,15 @@
         if sendto:
             try:
                 return self.handle_message(message)
+            except MailUsageHelp:
+                # bounce the message back to the sender with the usage message
+                fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
+                sendto = [sendto[0][1]]
+                m = ['']
+                m.append('\n\nMail Gateway Help\n=================')
+                m.append(fulldoc)
+                m = self.bounce_message(message, sendto, m,
+                    subject="Mail Gateway Help")
             except MailUsageError, value:
                 # bounce the message back to the sender with the usage message
                 fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
@@ -162,8 +174,11 @@
                 m = self.bounce_message(message, sendto, m)
             except:
                 # bounce the message back to the sender with the error message
-                sendto = [sendto[0][1]]
+                sendto = [sendto[0][1], self.instance.ADMIN_EMAIL]
                 m = ['']
+                m.append('An unexpected error occurred during the processing')
+                m.append('of your message. The tracker administrator is being')
+                m.append('notified.\n')
                 m.append('----  traceback of failure  ----')
                 s = cStringIO.StringIO()
                 import traceback
@@ -172,7 +187,7 @@
                 m = self.bounce_message(message, sendto, m)
         else:
             # very bad-looking message - we don't even know who sent it
-            sendto = [self.ADMIN_EMAIL]
+            sendto = [self.instance.ADMIN_EMAIL]
             m = ['Subject: badly formed message from mail gateway']
             m.append('')
             m.append('The mail gateway retrieved a message which has no From:')
@@ -185,11 +200,11 @@
         # now send the message
         if SENDMAILDEBUG:
             open(SENDMAILDEBUG, 'w').write('From: %s\nTo: %s\n%s\n'%(
-                self.ADMIN_EMAIL, ', '.join(sendto), m.getvalue()))
+                self.instance.ADMIN_EMAIL, ', '.join(sendto), m.getvalue()))
         else:
             try:
-                smtp = smtplib.SMTP(self.MAILHOST)
-                smtp.sendmail(self.ADMIN_EMAIL, sendto, m.getvalue())
+                smtp = smtplib.SMTP(self.instance.MAILHOST)
+                smtp.sendmail(self.instance.ADMIN_EMAIL, sendto, m.getvalue())
             except socket.error, value:
                 raise MailGWError, "Couldn't send error email: "\
                     "mailhost %s"%value
@@ -206,7 +221,7 @@
         writer = MimeWriter.MimeWriter(msg)
         writer.addheader('Subject', subject)
         writer.addheader('From', '%s <%s>'% (self.instance.INSTANCE_NAME,
-                                            self.ISSUE_TRACKER_EMAIL))
+                                            self.instance.ISSUE_TRACKER_EMAIL))
         writer.addheader('To', ','.join(sendto))
         writer.addheader('MIME-Version', '1.0')
         part = writer.startmultipartbody('mixed')
@@ -228,13 +243,17 @@
                 w.addheader(header_name, message.getheader(header_name))
         # now attach the message body
         body = w.startbody(content_type)
-        message.rewindbody()
-        body.write(message.fp.read())
+        try:
+            message.rewindbody()
+        except IOError:
+            body.write("*** couldn't include message body: read from pipe ***")
+        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 that caused the error')
+        part.addheader('Content-Description','Message you sent')
         part.addheader('Content-Transfer-Encoding', '7bit')
         body = part.startbody('message/rfc822')
         body.write(m.getvalue())
@@ -249,6 +268,10 @@
         '''
         # handle the subject line
         subject = message.getheader('subject', '')
+
+        if subject.strip() == 'help':
+            raise MailUsageHelp
+
         m = subject_re.match(subject)
         if not m:
             raise MailUsageError, '''
@@ -319,6 +342,7 @@
         args = m.group('args')
         if args:
             for prop in string.split(args, ';'):
+                # extract the property name and value
                 try:
                     key, value = prop.split('=')
                 except ValueError, message:
@@ -328,6 +352,8 @@
 
 Subject was: "%s"
 '''%(message, subject)
+
+                # ensure it's a valid property name
                 key = key.strip()
                 try:
                     proptype =  properties[key]
@@ -337,6 +363,8 @@
 
 Subject was: "%s"
 '''%(key, subject)
+
+                # convert the string value to a real property value
                 if isinstance(proptype, hyperdb.String):
                     props[key] = value.strip()
                 if isinstance(proptype, hyperdb.Password):
@@ -362,32 +390,43 @@
 Subject was: "%s"
 '''%(key, message, subject)
                 elif isinstance(proptype, hyperdb.Link):
-                    link = self.db.classes[proptype.classname]
-                    propkey = link.labelprop(default_to_id=1)
+                    linkcl = self.db.classes[proptype.classname]
+                    propkey = linkcl.labelprop(default_to_id=1)
                     try:
-                        props[key] = link.get(value.strip(), propkey)
-                    except:
-                        props[key] = link.lookup(value.strip())
+                        props[key] = linkcl.lookup(value)
+                    except KeyError, message:
+                        raise MailUsageError, '''
+Subject argument list contains an invalid value for %s.
+
+Error was: %s
+Subject was: "%s"
+'''%(key, message, subject)
                 elif isinstance(proptype, hyperdb.Multilink):
-                    link = self.db.classes[proptype.classname]
-                    propkey = link.labelprop(default_to_id=1)
-                    l = [x.strip() for x in value.split(',')]
-                    for item in l:
+                    # get the linked class
+                    linkcl = self.db.classes[proptype.classname]
+                    propkey = linkcl.labelprop(default_to_id=1)
+                    for item in value.split(','):
+                        item = item.strip()
                         try:
-                            v = link.get(item, propkey)
-                        except:
-                            v = link.lookup(item)
+                            item = linkcl.lookup(item)
+                        except KeyError, message:
+                            raise MailUsageError, '''
+Subject argument list contains an invalid value for %s.
+
+Error was: %s
+Subject was: "%s"
+'''%(key, message, subject)
                         if props.has_key(key):
-                            props[key].append(v)
+                            props[key].append(item)
                         else:
-                            props[key] = [v]
+                            props[key] = [item]
 
         #
         # handle the users
         #
 
         # Don't create users if ANONYMOUS_REGISTER is denied
-        if self.ANONYMOUS_REGISTER == 'deny':
+        if self.instance.ANONYMOUS_REGISTER == 'deny':
             create = 0
         else:
             create = 1
@@ -413,7 +452,7 @@
 
         # now update the recipients list
         recipients = []
-        tracker_email = self.ISSUE_TRACKER_EMAIL.lower()
+        tracker_email = self.instance.ISSUE_TRACKER_EMAIL.lower()
         for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
             r = recipient[1].strip().lower()
             if r == tracker_email or not r:
@@ -427,8 +466,8 @@
         inreplyto = message.getheader('in-reply-to') or ''
         # generate a messageid if there isn't one
         if not messageid:
-            messageid = "%s.%s.%s%s-%s"%(time.time(), random.random(),
-                classname, nodeid, self.MAIL_DOMAIN)
+            messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
+                classname, nodeid, self.instance.MAIL_DOMAIN)
 
         #
         # now handle the body - find the message
@@ -448,8 +487,28 @@
                 subtype = part.gettype()
                 if subtype == 'text/plain' and not content:
                     # add all text/plain parts to the message content
+                    # BUG (in code or comment) only add the first one. 
                     if content is None:
-                        content = part.fp.read()
+                        # try name on Content-Type
+                        # maybe add name to non text content ?
+                        name = part.getparam('name')
+                        # assume first part is the mail
+                        encoding = part.getencoding()
+                        if encoding == 'base64':
+                            # BUG: is base64 really used for text encoding or
+                            # are we inserting zip files here. 
+                            data = binascii.a2b_base64(part.fp.read())
+                        elif encoding == 'quoted-printable':
+                            # the quopri module wants to work with files
+                            decoded = cStringIO.StringIO()
+                            quopri.decode(part.fp, decoded)
+                            data = decoded.getvalue()
+                        elif encoding == 'uuencoded':
+                            data = binascii.a2b_uu(part.fp.read())
+                        else:
+                            # take it as text
+                            data = part.fp.read()
+                        content = data
                     else:
                         content = content + part.fp.read()
 
@@ -477,7 +536,6 @@
                     elif encoding == 'uuencoded':
                         data = binascii.a2b_uu(part.fp.read())
                     attachments.append((name, part.gettype(), data))
-
             if content is None:
                 raise MailUsageError, '''
 Roundup requires the submission to be plain text. The message parser could
@@ -510,8 +568,23 @@
 '''
 
         else:
-            content = message.fp.read()
-
+            encoding = message.getencoding()
+            if encoding == 'base64':
+                # BUG: is base64 really used for text encoding or
+                # are we inserting zip files here. 
+                data = binascii.a2b_base64(message.fp.read())
+            elif encoding == 'quoted-printable':
+                # the quopri module wants to work with files
+                decoded = cStringIO.StringIO()
+                quopri.decode(message.fp, decoded)
+                data = decoded.getvalue()
+            elif encoding == 'uuencoded':
+                data = binascii.a2b_uu(message.fp.read())
+            else:
+                # take it as text
+                data = message.fp.read()
+            content = data
+ 
         summary, content = parseContent(content)
 
         # 
@@ -519,6 +592,8 @@
         #
         files = []
         for (name, mime_type, data) in attachments:
+            if not name:
+                name = "unnamed"
             files.append(self.db.file.create(type=mime_type, name=name,
                 content=data))
 
@@ -542,9 +617,10 @@
                 except KeyError:
                     pass
                 else:
+                    current_status = cl.get(nodeid, 'status')
                     if (not props.has_key('status') and
-                            properties['status'] == unread_id or
-                            properties['status'] == resolved_id):
+                            current_status == unread_id or
+                            current_status == resolved_id):
                         props['status'] = chatting_id
 
             # add nosy in arguments to issue's nosy list
@@ -561,12 +637,10 @@
                 n[nid] = 1
             props['nosy'] = n.keys()
             # add assignedto to the nosy list
-            try:
-                assignedto = self.db.user.lookup(props['assignedto'])
+            if props.has_key('assignedto'):
+                assignedto = props['assignedto']
                 if assignedto not in props['nosy']:
                     props['nosy'].append(assignedto)
-            except:
-                pass
 
             message_id = self.db.msg.create(author=author,
                 recipients=recipients, date=date.Date('.'), summary=summary,
@@ -625,10 +699,7 @@
             nosy = props.get('nosy', [])
             n = {}
             for value in nosy:
-                if self.db.hasnode('user', value):
-                    nid = value
-                else:
-                    continue
+                nid = value
                 if n.has_key(nid): continue
                 n[nid] = 1
             props['nosy'] = n.keys()
@@ -645,13 +716,7 @@
 
             # add assignedto to the nosy list
             if properties.has_key('assignedto') and props.has_key('assignedto'):
-                try:
-                    assignedto = self.db.user.lookup(props['assignedto'])
-                except KeyError:
-                    raise MailUsageError, '''
-There was a problem with the message you sent:
-   Assignedto user '%s' doesn't exist
-'''%props['assignedto']
+                assignedto = props['assignedto']
                 if not n.has_key(assignedto):
                     props['nosy'].append(assignedto)
                     n[assignedto] = 1
@@ -693,21 +758,100 @@
         if not section:
             continue
         lines = eol.split(section)
-        if lines[0] and lines[0][0] in '>|':
-            continue
-        if len(lines) > 1 and lines[1] and lines[1][0] in '>|':
-            continue
+        if (lines[0] and lines[0][0] in '>|') or (len(lines) > 1 and
+                lines[1] and lines[1][0] in '>|'):
+            # 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 '>|':
+                    break
+            else:
+                # TODO: people who want to keep quoted bits will want the
+                # next line...
+                # 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)
+
         if not summary:
+            # if we don't have our summary yet use the first line of this
+            # section
             summary = lines[0]
-            l.append(section)
-            continue
-        if signature.match(lines[0]):
+        elif signature.match(lines[0]):
             break
+
+        # and add the section to the output
         l.append(section)
     return summary, '\n\n'.join(l)
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.62  2002/02/05 14:15:29  grubert
+#  . respect encodings in non multipart messages.
+#
+# Revision 1.61  2002/02/04 09:40:21  grubert
+#  . add test for multipart messages with first part being encoded.
+#
+# Revision 1.60  2002/02/01 07:43:12  grubert
+#  . mailgw checks encoding on first part too.
+#
+# Revision 1.59  2002/01/23 21:43:23  richard
+# tabnuke
+#
+# Revision 1.58  2002/01/23 21:41:56  richard
+#  . mailgw failures (unexpected ones) are forwarded to the roundup admin
+#
+# Revision 1.57  2002/01/22 22:27:43  richard
+#  . handle stripping of "AW:" from subject line
+#
+# Revision 1.56  2002/01/22 11:54:45  rochecompaan
+# Fixed status change in mail gateway.
+#
+# Revision 1.55  2002/01/21 10:05:47  rochecompaan
+# Feature:
+#  . the mail gateway now responds with an error message when invalid
+#    values for arguments are specified for link or multilink properties
+#  . modified unit test to check nosy and assignedto when specified as
+#    arguments
+#
+# Fixed:
+#  . fixed setting nosy as argument in subject line
+#
+# Revision 1.54  2002/01/16 09:14:45  grubert
+#  . if the attachment has no name, name it unnamed, happens with tnefs.
+#
+# Revision 1.53  2002/01/16 07:20:54  richard
+# simple help command for mailgw
+#
+# Revision 1.52  2002/01/15 00:12:40  richard
+# #503340 ] creating issue with [asignedto=p.ohly]
+#
+# Revision 1.51  2002/01/14 02:20:15  richard
+#  . changed all config accesses so they access either the instance or the
+#    config attriubute on the db. This means that all config is obtained from
+#    instance_config instead of the mish-mash of classes. This will make
+#    switching to a ConfigParser setup easier too, I hope.
+#
+# At a minimum, this makes migration a _little_ easier (a lot easier in the
+# 0.5.0 switch, I hope!)
+#
+# Revision 1.50  2002/01/11 22:59:01  richard
+#  . #502342 ] pipe interface
+#
+# Revision 1.49  2002/01/10 06:19:18  richard
+# followup lines directly after a quoted section were being eaten.
+#
+# Revision 1.48  2002/01/08 04:12:05  richard
+# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
+#
+# Revision 1.47  2002/01/02 02:32:38  richard
+# ANONYMOUS_ACCESS -> ANONYMOUS_REGISTER
+#
 # Revision 1.46  2002/01/02 02:31:38  richard
 # Sorry for the huge checkin message - I was only intending to implement #496356
 # but I found a number of places where things had been broken by transactions:

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