Code

...except of course it's nice to use valid Python syntax
[roundup.git] / roundup / mailgw.py
index ae9b9faef1cf974506027e5e8b50a2113f9a56df..ad9c10594e1aabf76365c9cf31997bc9d1c860a0 100644 (file)
@@ -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.69 2002-05-06 23:37:21 richard Exp $
+$Id: mailgw.py,v 1.75 2002-07-09 01:21:24 richard Exp $
 '''
 
 
 '''
 
 
@@ -128,6 +128,10 @@ class MailGW:
         self.instance = instance
         self.db = db
 
         self.instance = instance
         self.db = db
 
+        # should we trap exceptions (normal usage) or pass them through
+        # (for testing)
+        self.trapExceptions = 1
+
     def main(self, fp):
         ''' fp - the file from which to read the Message.
         '''
     def main(self, fp):
         ''' fp - the file from which to read the Message.
         '''
@@ -146,6 +150,8 @@ class MailGW:
         # its way into here... try to handle it gracefully
         sendto = message.getaddrlist('from')
         if sendto:
         # its way into here... try to handle it gracefully
         sendto = message.getaddrlist('from')
         if sendto:
+            if not self.trapExceptions:
+                return self.handle_message(message)
             try:
                 return self.handle_message(message)
             except MailUsageHelp:
             try:
                 return self.handle_message(message)
             except MailUsageHelp:
@@ -344,7 +350,7 @@ Subject was: "%s"
             title = ''
 
         # but we do need either a title or a nodeid...
             title = ''
 
         # but we do need either a title or a nodeid...
-        if not nodeid and not title:
+        if nodeid is None and not title:
             raise MailUsageError, '''
 I cannot match your message to a node in the database - you need to either
 supply a full node identifier (with number, eg "[issue123]" or keep the
             raise MailUsageError, '''
 I cannot match your message to a node in the database - you need to either
 supply a full node identifier (with number, eg "[issue123]" or keep the
@@ -353,105 +359,135 @@ previous subject title intact so I can match that.
 Subject was: "%s"
 '''%subject
 
 Subject was: "%s"
 '''%subject
 
-        # extract the args
-        subject_args = m.group('args')
-
         # If there's no nodeid, check to see if this is a followup and
         # maybe someone's responded to the initial mail that created an
         # entry. Try to find the matching nodes with the same title, and
         # use the _last_ one matched (since that'll _usually_ be the most
         # recent...)
         # If there's no nodeid, check to see if this is a followup and
         # maybe someone's responded to the initial mail that created an
         # entry. Try to find the matching nodes with the same title, and
         # use the _last_ one matched (since that'll _usually_ be the most
         # recent...)
-        if not nodeid and m.group('refwd'):
+        if nodeid is None and m.group('refwd'):
             l = cl.stringFind(title=title)
             if l:
                 nodeid = l[-1]
 
             l = cl.stringFind(title=title)
             if l:
                 nodeid = l[-1]
 
-        # start of the props
+        # if a nodeid was specified, make sure it's valid
+        if nodeid is not None and not cl.hasnode(nodeid):
+            raise MailUsageError, '''
+The node specified by the designator in the subject of your message ("%s")
+does not exist.
+
+Subject was: "%s"
+'''%(nodeid, subject)
+
+        #
+        # extract the args
+        #
+        subject_args = m.group('args')
+
+        #
+        # handle the subject argument list
+        #
+        # figure what the properties of this Class are
         properties = cl.getprops()
         props = {}
         properties = cl.getprops()
         props = {}
-
-        # handle the args
         args = m.group('args')
         if args:
         args = m.group('args')
         if args:
+            errors = []
             for prop in string.split(args, ';'):
                 # extract the property name and value
                 try:
             for prop in string.split(args, ';'):
                 # extract the property name and value
                 try:
-                    key, value = prop.split('=')
+                    propname, value = prop.split('=')
                 except ValueError, message:
                 except ValueError, message:
-                    raise MailUsageError, '''
-Subject argument list not of form [arg=value,value,...;arg=value,value...]
-   (specific exception message was "%s")
-
-Subject was: "%s"
-'''%(message, subject)
+                    errors.append('not of form [arg=value,'
+                        'value,...;arg=value,value...]')
+                    break
 
                 # ensure it's a valid property name
 
                 # ensure it's a valid property name
-                key = key.strip()
+                propname = propname.strip()
                 try:
                 try:
-                    proptype =  properties[key]
+                    proptype =  properties[propname]
                 except KeyError:
                 except KeyError:
-                    raise MailUsageError, '''
-Subject argument list refers to an invalid property: "%s"
-
-Subject was: "%s"
-'''%(key, subject)
+                    errors.append('refers to an invalid property: '
+                        '"%s"'%propname)
+                    continue
 
                 # convert the string value to a real property value
                 if isinstance(proptype, hyperdb.String):
 
                 # convert the string value to a real property value
                 if isinstance(proptype, hyperdb.String):
-                    props[key] = value.strip()
+                    props[propname] = value.strip()
                 if isinstance(proptype, hyperdb.Password):
                 if isinstance(proptype, hyperdb.Password):
-                    props[key] = password.Password(value.strip())
+                    props[propname] = password.Password(value.strip())
                 elif isinstance(proptype, hyperdb.Date):
                     try:
                 elif isinstance(proptype, hyperdb.Date):
                     try:
-                        props[key] = date.Date(value.strip())
+                        props[propname] = date.Date(value.strip())
                     except ValueError, message:
                     except ValueError, message:
-                        raise UsageError, '''
-Subject argument list contains an invalid date for %s.
-
-Error was: %s
-Subject was: "%s"
-'''%(key, message, subject)
+                        errors.append('contains an invalid date for '
+                            '%s.'%propname)
                 elif isinstance(proptype, hyperdb.Interval):
                     try:
                 elif isinstance(proptype, hyperdb.Interval):
                     try:
-                        props[key] = date.Interval(value) # no strip needed
+                        props[propname] = date.Interval(value)
                     except ValueError, message:
                     except ValueError, message:
-                        raise UsageError, '''
-Subject argument list contains an invalid date interval for %s.
-
-Error was: %s
-Subject was: "%s"
-'''%(key, message, subject)
+                        errors.append('contains an invalid date interval'
+                            'for %s.'%propname)
                 elif isinstance(proptype, hyperdb.Link):
                     linkcl = self.db.classes[proptype.classname]
                     propkey = linkcl.labelprop(default_to_id=1)
                     try:
                 elif isinstance(proptype, hyperdb.Link):
                     linkcl = self.db.classes[proptype.classname]
                     propkey = linkcl.labelprop(default_to_id=1)
                     try:
-                        props[key] = linkcl.lookup(value)
+                        props[propname] = linkcl.lookup(value)
                     except KeyError, message:
                     except KeyError, message:
-                        raise MailUsageError, '''
-Subject argument list contains an invalid value for %s.
-
-Error was: %s
-Subject was: "%s"
-'''%(key, message, subject)
+                        errors.append('"%s" is not a value for %s.'%(value,
+                            propname))
                 elif isinstance(proptype, hyperdb.Multilink):
                     # get the linked class
                     linkcl = self.db.classes[proptype.classname]
                     propkey = linkcl.labelprop(default_to_id=1)
                 elif isinstance(proptype, hyperdb.Multilink):
                     # get the linked class
                     linkcl = self.db.classes[proptype.classname]
                     propkey = linkcl.labelprop(default_to_id=1)
+                    if nodeid:
+                        curvalue = cl.get(nodeid, propname)
+                    else:
+                        curvalue = []
+
+                    # handle each add/remove in turn
                     for item in value.split(','):
                         item = item.strip()
                     for item in value.split(','):
                         item = item.strip()
+
+                        # handle +/-
+                        remove = 0
+                        if item.startswith('-'):
+                            remove = 1
+                            item = item[1:]
+                        elif item.startswith('+'):
+                            item = item[1:]
+
+                        # look up the value
                         try:
                             item = linkcl.lookup(item)
                         except KeyError, message:
                         try:
                             item = linkcl.lookup(item)
                         except KeyError, message:
-                            raise MailUsageError, '''
-Subject argument list contains an invalid value for %s.
+                            errors.append('"%s" is not a value for %s.'%(item,
+                                propname))
+                            continue
+
+                        # perform the add/remove
+                        if remove:
+                            try:
+                                curvalue.remove(item)
+                            except ValueError:
+                                errors.append('"%s" is not currently in '
+                                    'for %s.'%(item, propname))
+                                continue
+                        else:
+                            if item not in curvalue:
+                                curvalue.append(item)
+
+                    # that's it, set the new Multilink property value
+                    props[propname] = curvalue
+
+            # handle any errors parsing the argument list
+            if errors:
+                errors = '\n- '.join(errors)
+                raise MailUsageError, '''
+There were problems handling your subject line argument list:
+- %s
 
 
-Error was: %s
 Subject was: "%s"
 Subject was: "%s"
-'''%(key, message, subject)
-                        if props.has_key(key):
-                            props[key].append(item)
-                        else:
-                            props[key] = [item]
+'''%(errors, subject)
 
         #
         # handle the users
 
         #
         # handle the users
@@ -604,8 +640,13 @@ not find a text/plain part to use.
         else:
             content = self.get_part_data_decoded(message) 
  
         else:
             content = self.get_part_data_decoded(message) 
  
-        keep_citations = self.instance.EMAIL_KEEP_QUOTED_TEXT == 'yes'
-        keep_body = self.instance.EMAIL_LEAVE_BODY_UNCHANGED == 'yes'
+        # figure how much we should muck around with the email body
+        keep_citations = getattr(self.instance, 'EMAIL_KEEP_QUOTED_TEXT',
+            'no') == 'yes'
+        keep_body = getattr(self.instance, 'EMAIL_LEAVE_BODY_UNCHANGED',
+            'no') == 'yes'
+
+        # parse the body of the message, stripping out bits as appropriate
         summary, content = parseContent(content, keep_citations, 
             keep_body)
 
         summary, content = parseContent(content, keep_citations, 
             keep_body)
 
@@ -619,156 +660,53 @@ not find a text/plain part to use.
             files.append(self.db.file.create(type=mime_type, name=name,
                 content=data))
 
             files.append(self.db.file.create(type=mime_type, name=name,
                 content=data))
 
+        # 
+        # create the message if there's a message body (content)
         #
         #
-        # 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. 
-
-            # if the message is currently 'unread' or 'resolved', then set
-            # it to 'chatting'
-            if properties.has_key('status'):
-                try:
-                    # determine the id of 'unread', 'resolved' and 'chatting'
-                    unread_id = self.db.status.lookup('unread')
-                    resolved_id = self.db.status.lookup('resolved')
-                    chatting_id = self.db.status.lookup('chatting')
-                except KeyError:
-                    pass
-                else:
-                    current_status = cl.get(nodeid, 'status')
-                    if (not props.has_key('status') and
-                            current_status == unread_id or
-                            current_status == resolved_id):
-                        props['status'] = chatting_id
-
-            # update the nosy list
-            current = {}
-            for nid in cl.get(nodeid, 'nosy'):
-                current[nid] = 1
-            self.updateNosy(props, author, recipients, current)
-
-            # create the message
+        if content:
             message_id = self.db.msg.create(author=author,
                 recipients=recipients, date=date.Date('.'), summary=summary,
                 content=content, files=files, messageid=messageid,
                 inreplyto=inreplyto)
             message_id = self.db.msg.create(author=author,
                 recipients=recipients, date=date.Date('.'), summary=summary,
                 content=content, files=files, messageid=messageid,
                 inreplyto=inreplyto)
-            try:
+
+            # attach the message to the node
+            if nodeid:
+                # add the message to the node's list
                 messages = cl.get(nodeid, 'messages')
                 messages = cl.get(nodeid, 'messages')
-            except IndexError:
-                raise MailUsageError, '''
-The node specified by the designator in the subject of your message ("%s")
-does not exist.
+                messages.append(message_id)
+                props['messages'] = messages
+            else:
+                # pre-load the messages list
+                props['messages'] = [message_id]
 
 
-Subject was: "%s"
-'''%(nodeid, subject)
-            messages.append(message_id)
-            props['messages'] = messages
+                # set the title to the subject
+                if properties.has_key('title') and not props.has_key('title'):
+                    props['title'] = title
 
 
-            # now apply the changes
-            try:
+        #
+        # perform the node change / create
+        #
+        try:
+            if nodeid:
                 cl.set(nodeid, **props)
                 cl.set(nodeid, **props)
-            except (TypeError, IndexError, ValueError), message:
-                raise MailUsageError, '''
-There was a problem with the message you sent:
-   %s
-'''%message
-            # commit the changes to the DB
-            self.db.commit()
-        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, messageid=messageid,
-                inreplyto=inreplyto)
-
-            # pre-set the issue to unread
-            if properties.has_key('status') and not props.has_key('status'):
-                try:
-                    # determine the id of 'unread'
-                    unread_id = self.db.status.lookup('unread')
-                except KeyError:
-                    pass
-                else:
-                    props['status'] = '1'
-
-            # set the title to the subject
-            if properties.has_key('title') and not props.has_key('title'):
-                props['title'] = title
-
-            # pre-load the messages list
-            props['messages'] = [message_id]
-
-            # set up (clean) the nosy list
-            self.updateNosy(props, author, recipients)
-
-            # and attempt to create the new node
-            try:
+            else:
                 nodeid = cl.create(**props)
                 nodeid = cl.create(**props)
-            except (TypeError, IndexError, ValueError), message:
-                raise MailUsageError, '''
+        except (TypeError, IndexError, ValueError), message:
+            raise MailUsageError, '''
 There was a problem with the message you sent:
    %s
 '''%message
 
 There was a problem with the message you sent:
    %s
 '''%message
 
-            # commit the new node(s) to the DB
-            self.db.commit()
+        # commit the changes to the DB
+        self.db.commit()
 
         return nodeid
 
 
         return nodeid
 
-    def updateNosy(self, props, author, recipients, current=None):
-        '''Determine what the nosy list should be given:
-
-            props:      properties specified on the subject line of the message
-            author:     the sender of the message
-            recipients: the recipients (to, cc) of the message
-            current:    if the issue already exists, this is the current nosy
-                        list, as a dictionary.
-        '''
-        if current is None:
-            current = {}
-            ok = ('new', 'yes')
-        else:
-            ok = ('yes',)
-
-        # add nosy in arguments to issue's nosy list
-        nosy = props.get('nosy', [])
-        for value in nosy:
-            if not self.db.hasnode('user', value):
-                continue
-            if not current.has_key(value):
-                current[value] = 1
-
-        # add the author to the nosy list
-        if getattr(self.instance, 'ADD_AUTHOR_TO_NOSY', 'new') in ok:
-            if not current.has_key(author):
-                current[author] = 1
-
-        # add on the recipients of the message
-        if getattr(self.instance, 'ADD_RECIPIENTS_TO_NOSY', 'new') in ok:
-            for recipient in recipients:
-                if not current.has_key(recipient):
-                    current[recipient] = 1
-
-        # add assignedto to the nosy list
-        if props.has_key('assignedto'):
-            assignedto = props['assignedto']
-            if not current.has_key(assignedto):
-                current[assignedto] = 1
-
-        props['nosy'] = current.keys()
-
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'), 
         signature=re.compile(r'^[>|\s]*[-_]+\s*$'),
 def parseContent(content, keep_citations, keep_body,
         blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
         eol=re.compile(r'[\r\n]+'), 
         signature=re.compile(r'^[>|\s]*[-_]+\s*$'),
-        original_message=re.compile(r'^-----Original Message-----$')):
+        original_message=re.compile(r'^[>|\s]*-----Original Message-----$')):
     ''' 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
     ''' 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
@@ -833,6 +771,38 @@ def parseContent(content, keep_citations, keep_body,
 
 #
 # $Log: not supported by cvs2svn $
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.74  2002/05/29 01:16:17  richard
+# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
+# though.
+#
+# . #541941 ] changing multilink properties by mail
+# . #526730 ] search for messages capability
+# . #505180 ] split MailGW.handle_Message
+#   - also changed cgi client since it was duplicating the functionality
+# . build htmlbase if tests are run using CVS checkout (removed note from
+#   installation.txt)
+# . don't create an empty message on email issue creation if the email is empty
+#
+# Revision 1.73  2002/05/22 04:12:05  richard
+#  . applied patch #558876 ] cgi client customization
+#    ... with significant additions and modifications ;)
+#    - extended handling of ML assignedto to all places it's handled
+#    - added more NotFound info
+#
+# Revision 1.72  2002/05/22 01:24:51  richard
+# Added note to MIGRATION about new config vars. Also made us more resilient
+# for upgraders. Reinstated list header style (oops)
+#
+# Revision 1.71  2002/05/08 02:40:55  richard
+# grr
+#
+# Revision 1.70  2002/05/06 23:40:07  richard
+# hrm
+#
+# Revision 1.69  2002/05/06 23:37:21  richard
+# Tweaking the signature deletion from mail messages.
+# Added nuking of the "-----Original Message-----" crap from Outlook.
+#
 # Revision 1.68  2002/05/02 07:56:34  richard
 # . added option to automatically add the authors and recipients of messages
 #   to the nosy lists with the options ADD_AUTHOR_TO_NOSY (default 'new') and
 # Revision 1.68  2002/05/02 07:56:34  richard
 # . added option to automatically add the authors and recipients of messages
 #   to the nosy lists with the options ADD_AUTHOR_TO_NOSY (default 'new') and