diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index 988b459e7402c61d83b6144a73c5a52717c6ce50..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.
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.58 2002-01-23 21:41:56 richard Exp $
+$Id: mailgw.py,v 1.73 2002-05-22 04:12:05 richard Exp $
'''
'''
return Message(s)
subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re|aw)\s*\W?\s*)*'
return Message(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<classname>[^\d\s]+)(?P<nodeid>\d+)?\])?'
r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I)
class MailGW:
r'\s*(?P<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I)
class MailGW:
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.
'''
- self.handle_Message(Message(fp))
+ return self.handle_Message(Message(fp))
def handle_Message(self, message):
'''Handle an RFC822 Message
def handle_Message(self, message):
'''Handle an RFC822 Message
# bounce the message back to the sender with the error message
sendto = [sendto[0][1], self.instance.ADMIN_EMAIL]
m = ['']
# bounce the message back to the sender with the error message
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('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
m.append('---- traceback of failure ----')
s = cStringIO.StringIO()
import traceback
writer.lastpart()
return msg
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
def handle_message(self, message):
''' message - a Message instance
raise MailUsageHelp
m = subject_re.match(subject)
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
if not m:
raise MailUsageError, '''
The message you sent to roundup did not contain a properly formed subject
Subject was: "%s"
'''%subject
Subject was: "%s"
'''%subject
- # get the classname
- classname = m.group('classname')
+ # get the class
try:
cl = self.db.getclass(classname)
except KeyError:
try:
cl = self.db.getclass(classname)
except KeyError:
# handle the users
#
# 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
create = 0
- else:
- create = 1
+
author = self.db.uidFromAddress(message.getaddrlist('from')[0],
create=create)
if not author:
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
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
#
# handle message-id and in-reply-to
#
content_type = message.gettype()
attachments = []
#
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()
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:
# 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
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()))
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
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))
attachments.append((name, part.gettype(), data))
-
if content is None:
raise MailUsageError, '''
Roundup requires the submission to be plain text. The message parser could
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:
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
if content is None:
raise MailUsageError, '''
Roundup requires the submission to be plain text. The message parser could
'''
else:
'''
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
#
# handle the attachments
current_status == resolved_id):
props['status'] = chatting_id
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'):
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,
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
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:
# and attempt to create the new node
try:
# commit the new node(s) to the DB
self.db.commit()
# 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
''' 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:
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:
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]
# 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)
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 $
#
# $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.57 2002/01/22 22:27:43 richard
# . handle stripping of "AW:" from subject line
#