X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fmailgw.py;h=4c99afb0a22de18a65b7283dbb4032c2a92a10e6;hb=f04ca7e8c6067e05296508ed2b7c302425ffbcc8;hp=a47db26c0a6e6348a891974ac973aa2c38bf20f1;hpb=fb4ab934a3f43f5582fe58db47abcf468354c68f;p=roundup.git diff --git a/roundup/mailgw.py b/roundup/mailgw.py index a47db26..4c99afb 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -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.55 2002-01-21 10:05:47 rochecompaan Exp $ +$Id: mailgw.py,v 1.73 2002-05-22 04:12:05 richard Exp $ ''' @@ -119,8 +119,8 @@ class Message(mimetools.Message): s.seek(0) return Message(s) -subject_re = re.compile(r'(?P\s*\W?\s*(fwd|re)\s*\W?\s*)*' - r'\s*(\[(?P[^\d\s]+)(?P\d+)?\])' +subject_re = re.compile(r'(?P\s*\W?\s*(fwd|re|aw)\s*\W?\s*)*' + r'\s*(\[(?P[^\d\s]+)(?P\d+)?\])?' r'\s*(?P[^[]+)?(\[(?P<args>.+?)\])?', re.I) class MailGW: @@ -131,7 +131,7 @@ class MailGW: def main(self, fp): ''' fp - the file from which to read the Message. ''' - self.handle_Message(Message(fp)) + return self.handle_Message(Message(fp)) def handle_Message(self, message): '''Handle an RFC822 Message @@ -174,8 +174,11 @@ class MailGW: 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 @@ -258,6 +261,25 @@ class MailGW: writer.lastpart() return msg + def get_part_data_decoded(self,part): + encoding = part.getencoding() + data = None + 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() + return data + def handle_message(self, message): ''' message - a Message instance @@ -270,6 +292,20 @@ class MailGW: raise MailUsageHelp m = subject_re.match(subject) + + # check for well-formed subject line + if m: + # get the classname + classname = m.group('classname') + if classname is None: + # no classname, fallback on the default + if hasattr(self.instance, 'MAIL_DEFAULT_CLASS') and \ + self.instance.MAIL_DEFAULT_CLASS: + classname = self.instance.MAIL_DEFAULT_CLASS + else: + # fail + m = None + if not m: raise MailUsageError, ''' The message you sent to roundup did not contain a properly formed subject @@ -285,8 +321,7 @@ line. The subject must contain a class name or designator to indicate the Subject was: "%s" '''%subject - # get the classname - classname = m.group('classname') + # get the class try: cl = self.db.getclass(classname) except KeyError: @@ -422,11 +457,15 @@ Subject was: "%s" # handle the users # - # Don't create users if ANONYMOUS_REGISTER is denied - if self.instance.ANONYMOUS_REGISTER == 'deny': + # Don't create users if ANONYMOUS_REGISTER_MAIL is denied + # ... fall back on ANONYMOUS_REGISTER if the other doesn't exist + create = 1 + if hasattr(self.instance, 'ANONYMOUS_REGISTER_MAIL'): + if self.instance.ANONYMOUS_REGISTER_MAIL == 'deny': + create = 0 + elif self.instance.ANONYMOUS_REGISTER == 'deny': create = 0 - else: - create = 1 + author = self.db.uidFromAddress(message.getaddrlist('from')[0], create=create) if not author: @@ -454,7 +493,14 @@ Unknown address: %s r = recipient[1].strip().lower() if r == tracker_email or not r: continue - recipients.append(self.db.uidFromAddress(recipient)) + + # look up the recipient - create if necessary (and we're + # allowed to) + recipient = self.db.uidFromAddress(recipient, create) + + # if all's well, add the recipient to the list + if recipient: + recipients.append(recipient) # # handle message-id and in-reply-to @@ -471,6 +517,32 @@ Unknown address: %s # content_type = message.gettype() attachments = [] + # General multipart handling: + # Take the first text/plain part, anything else is considered an + # attachment. + # multipart/mixed: multiple "unrelated" parts. + # multipart/signed (rfc 1847): + # The control information is carried in the second of the two + # required body parts. + # ACTION: Default, so if content is text/plain we get it. + # multipart/encrypted (rfc 1847): + # The control information is carried in the first of the two + # required body parts. + # ACTION: Not handleable as the content is encrypted. + # multipart/related (rfc 1872, 2112, 2387): + # The Multipart/Related content-type addresses the MIME + # representation of compound objects. + # ACTION: Default. If we are lucky there is a text/plain. + # TODO: One should use the start part and look for an Alternative + # that is text/plain. + # multipart/Alternative (rfc 1872, 1892): + # only in "related" ? + # multipart/report (rfc 1892): + # e.g. mail system delivery status reports. + # ACTION: Default. Could be ignored or used for Delivery Notification + # flagging. + # multipart/form-data: + # For web forms only. if content_type == 'multipart/mixed': # skip over the intro to the first boundary part = message.getPart() @@ -483,12 +555,8 @@ Unknown address: %s # parse it subtype = part.gettype() if subtype == 'text/plain' and not content: - # add all text/plain parts to the message content - if content is None: - content = part.fp.read() - else: - content = content + part.fp.read() - + # The first text/plain part is the message content. + content = self.get_part_data_decoded(part) elif subtype == 'message/rfc822': # handle message/rfc822 specially - the name should be # the subject of the actual e-mail embedded here @@ -497,23 +565,12 @@ Unknown address: %s name = mailmess.getheader('subject') part.fp.seek(i) attachments.append((name, 'message/rfc822', part.fp.read())) - else: # try name on Content-Type name = part.getparam('name') # this is just an attachment - encoding = part.getencoding() - if encoding == 'base64': - 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()) + data = self.get_part_data_decoded(part) attachments.append((name, part.gettype(), data)) - if content is None: raise MailUsageError, ''' Roundup requires the submission to be plain text. The message parser could @@ -531,8 +588,7 @@ not find a text/plain part to use. break # parse it if part.gettype() == 'text/plain' and not content: - # this one's our content - content = part.fp.read() + content = self.get_part_data_decoded(part) if content is None: raise MailUsageError, ''' Roundup requires the submission to be plain text. The message parser could @@ -546,9 +602,17 @@ not find a text/plain part to use. ''' else: - content = message.fp.read() - - summary, content = parseContent(content) + content = self.get_part_data_decoded(message) + + # 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) # # handle the attachments @@ -580,30 +644,19 @@ not find a text/plain part to use. 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 - if not props.has_key('nosy'): props['nosy'] = [] - n = {} + # update the nosy list + current = {} for nid in cl.get(nodeid, 'nosy'): - n[nid] = 1 - for value in props['nosy']: - if self.db.hasnode('user', value): - nid = value - else: - continue - if n.has_key(nid): continue - n[nid] = 1 - props['nosy'] = n.keys() - # add assignedto to the nosy list - if props.has_key('assignedto'): - assignedto = props['assignedto'] - if assignedto not in props['nosy']: - props['nosy'].append(assignedto) + current[nid] = 1 + self.updateNosy(cl, props, author, recipients, current) + # create the message message_id = self.db.msg.create(author=author, recipients=recipients, date=date.Date('.'), summary=summary, content=content, files=files, messageid=messageid, @@ -658,30 +711,7 @@ There was a problem with the message you sent: props['messages'] = [message_id] # set up (clean) the nosy list - nosy = props.get('nosy', []) - n = {} - for value in nosy: - nid = value - if n.has_key(nid): continue - n[nid] = 1 - props['nosy'] = n.keys() - # add on the recipients of the message - for recipient in recipients: - if not n.has_key(recipient): - props['nosy'].append(recipient) - n[recipient] = 1 - - # add the author to the nosy list - if not n.has_key(author): - props['nosy'].append(author) - n[author] = 1 - - # add assignedto to the nosy list - if properties.has_key('assignedto') and props.has_key('assignedto'): - assignedto = props['assignedto'] - if not n.has_key(assignedto): - props['nosy'].append(assignedto) - n[assignedto] = 1 + self.updateNosy(cl, props, author, recipients) # and attempt to create the new node try: @@ -695,8 +725,60 @@ There was a problem with the message you sent: # commit the new node(s) to the DB self.db.commit() -def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), - eol=re.compile(r'[\r\n]+'), signature=re.compile(r'^[>|\s]*[-_]+\s*$')): + return nodeid + + def updateNosy(self, cl, 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(s) to the nosy list + if props.has_key('assignedto'): + propdef = cl.getprops() + if isinstance(propdef['assignedto'], hyperdb.Link): + assignedto_ids = [props['assignedto']] + elif isinstance(propdef['assignedto'], hyperdb.Multilink): + assignedto_ids = props['assignedto'] + for assignedto_id in assignedto_ids: + if not current.has_key(assignedto_id): + current[assignedto_id] = 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*$'), + 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 @@ -728,9 +810,9 @@ def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), if line[0] not in '>|': break else: - # TODO: people who want to keep quoted bits will want the - # next line... - # l.append(section) + # we keep quoted bits if specified in the config + if keep_citations: + l.append(section) continue # keep this section - it has reponse stuff in it if not summary: @@ -744,15 +826,95 @@ def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), # if we don't have our summary yet use the first line of this # section summary = lines[0] - elif signature.match(lines[0]): + elif signature.match(lines[0]) and 2 <= len(lines) <= 10: + # lose any signature + break + elif original_message.match(lines[0]): + # ditch the stupid Outlook quoting of the entire original message break # and add the section to the output l.append(section) - return summary, '\n\n'.join(l) + # we only set content for those who want to delete cruft from the + # message body, otherwise the body is left untouched. + if not keep_body: + content = '\n\n'.join(l) + return summary, content # # $Log: not supported by cvs2svn $ +# 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 +# ADD_RECIPIENTS_TO_NOSY (default 'new'). These settings emulate the current +# behaviour. Setting them to 'yes' will add the author/recipients to the nosy +# on messages that create issues and followup messages. +# . added missing documentation for a few of the config option values +# +# Revision 1.67 2002/04/23 15:46:49 rochecompaan +# . stripping of the email message body can now be controlled through +# the config variables EMAIL_KEEP_QUOTED_TEST and +# EMAIL_LEAVE_BODY_UNCHANGED. +# +# Revision 1.66 2002/03/14 23:59:24 richard +# . #517734 ] web header customisation is obscure +# +# Revision 1.65 2002/02/15 00:13:38 richard +# . #503204 ] mailgw needs a default class +# - partially done - the setting of additional properties can wait for a +# better configuration system. +# +# Revision 1.64 2002/02/14 23:46:02 richard +# . #516883 ] mail interface + ANONYMOUS_REGISTER +# +# Revision 1.63 2002/02/12 08:08:55 grubert +# . Clean up mail handling, multipart handling. +# +# 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. #