diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index a47db26c0a6e6348a891974ac973aa2c38bf20f1..4c99afb0a22de18a65b7283dbb4032c2a92a10e6 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
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 $
'''
s.seek(0)
return Message(s)
-subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re)\s*\W?\s*)*'
- r'\s*(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])'
+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)
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
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
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
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
Subject was: "%s"
'''%subject
- # get the classname
- classname = m.group('classname')
+ # get the class
try:
cl = self.db.getclass(classname)
except KeyError:
# 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:
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
#
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()
# 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
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
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
'''
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
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,
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:
# 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
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:
# 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.
#