diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index 9f339e7a078bb96ad2ac0f5d114a3ec92a790f01..9de891be5ccd136bbbc6de31f5b9800ab533c9bb 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-__doc__ = '''
+'''
An e-mail gateway for Roundup.
Incoming messages are examined for multiple parts:
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.89 2002-09-25 02:10:24 richard Exp $
+$Id: mailgw.py,v 1.102 2002-12-11 01:52:20 richard Exp $
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
class MailUsageHelp(Exception):
pass
+class MailLoop(Exception):
+ ''' We've seen this message before... '''
+ pass
+
class Unauthorized(Exception):
""" Access denied """
s.seek(0)
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<title>[^[]+)?(\[(?P<args>.+?)\])?', re.I)
+subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fwd|re|aw)\W\s*)*'
+ r'\s*(?P<quote>")?(\[(?P<classname>[^\d\s]+)(?P<nodeid>\d+)?\])?'
+ r'\s*(?P<title>[^[]+)?"?(\[(?P<args>.+?)\])?', re.I)
class MailGW:
def __init__(self, instance, db):
def do_pipe(self):
''' Read a message from standard input and pass it to the mail handler.
+
+ Read into an internal structure that we can seek on (in case
+ there's an error).
+
+ XXX: we may want to read this into a temporary file instead...
'''
- self.main(sys.stdin)
+ s = cStringIO.StringIO()
+ s.write(sys.stdin.read())
+ s.seek(0)
+ self.main(s)
return 0
def do_mailbox(self, filename):
m = ['']
m.append(str(value))
m = self.bounce_message(message, sendto, m)
+ except MailLoop:
+ # XXX we should use a log file here...
+ return
except:
# bounce the message back to the sender with the error message
+ # XXX we should use a log file here...
sendto = [sendto[0][1], self.instance.config.ADMIN_EMAIL]
m = ['']
m.append('An unexpected error occurred during the processing')
m = self.bounce_message(message, sendto, m)
else:
# very bad-looking message - we don't even know who sent it
+ # XXX we should use a log file here...
sendto = [self.instance.config.ADMIN_EMAIL]
m = ['Subject: badly formed message from mail gateway']
m.append('')
'''
msg = cStringIO.StringIO()
writer = MimeWriter.MimeWriter(msg)
+ writer.addheader('X-Roundup-Loop', 'hello')
writer.addheader('Subject', subject)
writer.addheader('From', '%s <%s>'% (self.instance.config.TRACKER_NAME,
self.instance.config.TRACKER_EMAIL))
writer.addheader('To', ','.join(sendto))
+ writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
+ time.gmtime()))
writer.addheader('MIME-Version', '1.0')
part = writer.startmultipartbody('mixed')
part = writer.nextpart()
body = part.startbody('text/plain')
body.write('\n'.join(error))
- # reconstruct the original message
- m = cStringIO.StringIO()
- w = MimeWriter.MimeWriter(m)
- # default the content_type, just in case...
- content_type = 'text/plain'
- # add the headers except the content-type
+ # attach the original message to the returned message
+ part = writer.nextpart()
+ part.addheader('Content-Disposition','attachment')
+ part.addheader('Content-Description','Message you sent')
+ body = part.startbody('text/plain')
for header in message.headers:
- header_name = header.split(':')[0]
- if header_name.lower() == 'content-type':
- content_type = message.getheader(header_name)
- elif message.getheader(header_name):
- w.addheader(header_name, message.getheader(header_name))
- # now attach the message body
- body = w.startbody(content_type)
+ body.write(header)
+ body.write('\n')
try:
message.rewindbody()
- except IOError:
- body.write("*** couldn't include message body: read from pipe ***")
+ except IOError, message:
+ body.write("*** couldn't include message body: %s ***"%message)
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 you sent')
- part.addheader('Content-Transfer-Encoding', '7bit')
- body = part.startbody('message/rfc822')
- body.write(m.getvalue())
-
writer.lastpart()
return msg
Parse the message as per the module docstring.
'''
+ # detect loops
+ if message.getheader('x-roundup-loop', ''):
+ raise MailLoop
+
# handle the subject line
subject = message.getheader('subject', '')
else:
title = ''
+ # strip off the quotes that dumb emailers put around the subject, like
+ # Re: "[issue1] bla blah"
+ if m.group('quote') and title.endswith('"'):
+ title = title[:-1]
+
# but we do need either a title or a nodeid...
if nodeid is None and not title:
raise MailUsageError, '''
author = uidFromAddress(self.db, message.getaddrlist('from')[0],
create=create)
- # no author? means we're not author
+ # if we're not recognised, and we don't get added as a user, then we
+ # must be anonymous
if not author:
- raise Unauthorized, '''
+ author = anonid
+
+ # make sure the author has permission to use the email interface
+ if not self.db.security.hasPermission('Email Access', author):
+ if author == anonid:
+ # we're anonymous and we need to be a registered user
+ raise Unauthorized, '''
You are not a registered user.
Unknown address: %s
'''%message.getaddrlist('from')[0][1]
-
- # make sure the author has permission to use the email interface
- if not self.db.security.hasPermission('Email Access', author):
- raise Unauthorized, 'You are not permitted to access this tracker.'
+ else:
+ # we're registered and we're _still_ not allowed access
+ raise Unauthorized, 'You are not permitted to access '\
+ 'this tracker.'
# make sure they're allowed to edit this class of information
if not self.db.security.hasPermission('Edit', author, classname):
attachments.append((name, 'message/rfc822', part.fp.read()))
else:
# try name on Content-Type
- name = part.getparam('name')
+ name = part.getparam('name').strip()
+ if not name:
+ disp = part.getheader('content-disposition', None)
+ if disp:
+ name = disp.getparam('filename').strip()
# this is just an attachment
data = self.get_part_data_decoded(part)
attachments.append((name, part.gettype(), data))
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',
+ keep_citations = getattr(self.instance.config, 'EMAIL_KEEP_QUOTED_TEXT',
'no') == 'yes'
- keep_body = getattr(self.instance, 'EMAIL_LEAVE_BODY_UNCHANGED',
+ keep_body = getattr(self.instance.config, 'EMAIL_LEAVE_BODY_UNCHANGED',
'no') == 'yes'
# parse the body of the message, stripping out bits as appropriate
# try the user alternate addresses if possible
props = db.user.getprops()
if props.has_key('alternate_addresses'):
- users = db.user.filter(None, {'alternate_addresses': address},
- [], [])
+ users = db.user.filter(None, {'alternate_addresses': address})
user = extractUserFromList(db.user, users)
if user is not None: return user
# 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 '>|':
+ if line and line[0] not in '>|':
break
else:
# we keep quoted bits if specified in the config
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)
+ # and while we're at it, use the first non-quoted bit as
+ # our summary
+ summary = section
if not summary:
# if we don't have our summary yet use the first line of this
# section
- summary = lines[0]
+ summary = section
elif signature.match(lines[0]) and 2 <= len(lines) <= 10:
# lose any signature
break
# and add the section to the output
l.append(section)
+ # figure the summary - find the first sentence-ending punctuation or the
+ # first whole line, whichever is longest
+ sentence = re.search(r'^([^!?\.]+[!?\.])', summary)
+ if sentence:
+ sentence = sentence.group(1)
+ else:
+ sentence = ''
+ first = eol.split(summary)[0]
+ summary = max(sentence, first)
+
# Now reconstitute the message content minus the bits we don't care
# about.
if not keep_body: