diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index 6ba384201701fb4cca1514a3e24c72f655948204..50e3cbed5b80cee753f44e95c563798f1b0efc8a 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
# under the same terms as Python, so long as this copyright message and
# disclaimer are retained in their original form.
#
-# IN NO EVENT SHALL THE BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# 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.9 2001-08-07 00:15:51 richard Exp $
+$Id: mailgw.py,v 1.85 2002-09-10 03:01:18 richard Exp $
'''
-
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
-import traceback
-import date
+import time, random
+import traceback, MimeWriter
+import hyperdb, date, password
+
+SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
+
+class MailGWError(ValueError):
+ pass
+
+class MailUsageError(ValueError):
+ pass
+
+class MailUsageHelp(Exception):
+ pass
+
+class Unauthorized(Exception):
+ """ Access denied """
+
+def initialiseSecurity(security):
+ ''' Create some Permissions and Roles on the security object
+
+ This function is directly invoked by security.Security.__init__()
+ as a part of the Security object instantiation.
+ '''
+ security.addPermission(name="Email Registration",
+ description="Anonymous may register through e-mail")
+ p = security.addPermission(name="Email Access",
+ description="User may use the email interface")
+ security.addPermissionToRole('Admin', p)
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
s.seek(0)
return Message(s)
-subject_re = re.compile(r'(\[?(fwd|re):\s*)*'
- r'(\[(?P<classname>[^\d]+)(?P<nodeid>\d+)?\])'
- r'(?P<title>[^\[]+)(\[(?P<args>.+?)\])?', re.I)
+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 __init__(self, db):
+ def __init__(self, instance, 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.
+ '''
+ return self.handle_Message(Message(fp))
- Read a message from fp and then call handle_message() with the
- result. This method's job is to make that call and handle any
+ def handle_Message(self, message):
+ '''Handle an RFC822 Message
+
+ Handle the Message object by calling handle_message() and then cope
+ with any errors raised by handle_message.
+ This method's job is to make that call and handle any
errors in a sane manner. It should be replaced if you wish to
handle errors in a different manner.
'''
- # ok, figure the subject, author, recipients and content-type
- message = Message(fp)
- try:
- self.handle_message(message)
- except:
- # bounce the message back to the sender with the error message
- sendto = [message.getaddrlist('from')[0][1]]
- m = ['Subject: failed issue tracker submission']
- m.append('')
- # TODO as attachments?
- m.append('---- traceback of failure ----')
- s = cStringIO.StringIO()
- import traceback
- traceback.print_exc(None, s)
- m.append(s.getvalue())
- m.append('---- failed message follows ----')
+ # in some rare cases, a particularly stuffed-up e-mail will make
+ # 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:
- fp.seek(0)
+ return self.handle_message(message)
+ except MailUsageHelp:
+ # bounce the message back to the sender with the usage message
+ fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
+ sendto = [sendto[0][1]]
+ m = ['']
+ m.append('\n\nMail Gateway Help\n=================')
+ m.append(fulldoc)
+ m = self.bounce_message(message, sendto, m,
+ subject="Mail Gateway Help")
+ except MailUsageError, value:
+ # bounce the message back to the sender with the usage message
+ fulldoc = '\n'.join(string.split(__doc__, '\n')[2:])
+ sendto = [sendto[0][1]]
+ m = ['']
+ m.append(str(value))
+ m.append('\n\nMail Gateway Help\n=================')
+ m.append(fulldoc)
+ m = self.bounce_message(message, sendto, m)
+ except Unauthorized, value:
+ # just inform the user that he is not authorized
+ sendto = [sendto[0][1]]
+ m = ['']
+ m.append(str(value))
+ m = self.bounce_message(message, sendto, m)
except:
- pass
- m.append(fp.read())
+ # bounce the message back to the sender with the error message
+ sendto = [sendto[0][1], self.instance.config.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
+ traceback.print_exc(None, s)
+ m.append(s.getvalue())
+ m = self.bounce_message(message, sendto, m)
+ else:
+ # very bad-looking message - we don't even know who sent it
+ sendto = [self.instance.config.ADMIN_EMAIL]
+ m = ['Subject: badly formed message from mail gateway']
+ m.append('')
+ m.append('The mail gateway retrieved a message which has no From:')
+ m.append('line, indicating that it is corrupt. Please check your')
+ m.append('mail gateway source. Failed message is attached.')
+ m.append('')
+ m = self.bounce_message(message, sendto, m,
+ subject='Badly formed message from mail gateway')
+
+ # now send the message
+ if SENDMAILDEBUG:
+ open(SENDMAILDEBUG, 'w').write('From: %s\nTo: %s\n%s\n'%(
+ self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
+ m.getvalue()))
+ else:
try:
- smtp = smtplib.SMTP(self.MAILHOST)
- smtp.sendmail(self.ADMIN_EMAIL, sendto, '\n'.join(m))
+ smtp = smtplib.SMTP(self.instance.config.MAILHOST)
+ smtp.sendmail(self.instance.config.ADMIN_EMAIL, sendto,
+ m.getvalue())
except socket.error, value:
- return "Couldn't send confirmation email: mailhost %s"%value
+ raise MailGWError, "Couldn't send error email: "\
+ "mailhost %s"%value
except smtplib.SMTPException, value:
- return "Couldn't send confirmation email: %s"%value
+ raise MailGWError, "Couldn't send error email: %s"%value
+
+ def bounce_message(self, message, sendto, error,
+ subject='Failed issue tracker submission'):
+ ''' create a message that explains the reason for the failed
+ issue submission to the author and attach the original
+ message.
+ '''
+ msg = cStringIO.StringIO()
+ writer = MimeWriter.MimeWriter(msg)
+ 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('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
+ 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)
+ try:
+ message.rewindbody()
+ except IOError:
+ body.write("*** couldn't include message body: read from pipe ***")
+ 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
+
+ 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
Parse the message as per the module docstring.
'''
# handle the subject line
- m = subject_re.match(message.getheader('subject'))
+ subject = message.getheader('subject', '')
+
+ if subject.strip() == 'help':
+ 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.config, 'MAIL_DEFAULT_CLASS') and \
+ self.instance.config.MAIL_DEFAULT_CLASS:
+ classname = self.instance.config.MAIL_DEFAULT_CLASS
+ else:
+ # fail
+ m = None
+
if not m:
- raise ValueError, 'No [designator] found in subject "%s"'
- classname = m.group('classname')
+ raise MailUsageError, '''
+The message you sent to roundup did not contain a properly formed subject
+line. The subject must contain a class name or designator to indicate the
+"topic" of the message. For example:
+ Subject: [issue] This is a new issue
+ - this will create a new issue in the tracker with the title "This is
+ a new issue".
+ Subject: [issue1234] This is a followup to issue 1234
+ - this will append the message's contents to the existing issue 1234
+ in the tracker.
+
+Subject was: "%s"
+'''%subject
+
+ # get the class
+ try:
+ cl = self.db.getclass(classname)
+ except KeyError:
+ raise MailUsageError, '''
+The class name you identified in the subject line ("%s") does not exist in the
+database.
+
+Valid class names are: %s
+Subject was: "%s"
+'''%(classname, ', '.join(self.db.getclasses()), subject)
+
+ # get the optional nodeid
nodeid = m.group('nodeid')
- title = m.group('title').strip()
+
+ # title is optional too
+ title = m.group('title')
+ if title:
+ title = title.strip()
+ else:
+ title = ''
+
+ # but we do need either a title or a nodeid...
+ 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
+previous subject title intact so I can match that.
+
+Subject was: "%s"
+'''%subject
+
+ # 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 nodeid is None and m.group('refwd'):
+ l = cl.stringFind(title=title)
+ if l:
+ nodeid = l[-1]
+
+ # 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')
- cl = self.db.getclass(classname)
+
+ #
+ # handle the subject argument list
+ #
+ # figure what the properties of this Class are
properties = cl.getprops()
props = {}
args = m.group('args')
if args:
- for prop in string.split(m.group('args'), ';'):
+ errors = []
+ for prop in string.split(args, ';'):
+ # extract the property name and value
try:
- key, value = prop.split('=')
+ propname, value = prop.split('=')
except ValueError, message:
- raise ValueError, 'Args list not of form [arg=value,value,...;arg=value,value,value..] (specific exception message was "%s")'%message
- type = properties[key]
- if type.isStringType:
- props[key] = value
- elif type.isDateType:
- props[key] = date.Date(value)
- elif type.isIntervalType:
- props[key] = date.Interval(value)
- elif type.isLinkType:
- props[key] = value
- elif type.isMultilinkType:
- props[key] = value.split(',')
+ errors.append('not of form [arg=value,'
+ 'value,...;arg=value,value...]')
+ break
+
+ # ensure it's a valid property name
+ propname = propname.strip()
+ try:
+ proptype = properties[propname]
+ except KeyError:
+ errors.append('refers to an invalid property: '
+ '"%s"'%propname)
+ continue
+
+ # convert the string value to a real property value
+ if isinstance(proptype, hyperdb.String):
+ props[propname] = value.strip()
+ if isinstance(proptype, hyperdb.Password):
+ props[propname] = password.Password(value.strip())
+ elif isinstance(proptype, hyperdb.Date):
+ try:
+ props[propname] = date.Date(value.strip())
+ except ValueError, message:
+ errors.append('contains an invalid date for '
+ '%s.'%propname)
+ elif isinstance(proptype, hyperdb.Interval):
+ try:
+ props[propname] = date.Interval(value)
+ except ValueError, message:
+ 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:
+ props[propname] = linkcl.lookup(value)
+ except KeyError, message:
+ 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)
+ if nodeid:
+ curvalue = cl.get(nodeid, propname)
+ else:
+ curvalue = []
+
+ # handle each add/remove in turn
+ # keep an extra list for all items that are
+ # definitely in the new list (in case of e.g.
+ # <propname>=A,+B, which should replace the old
+ # list with A,B)
+ set = 0
+ newvalue = []
+ 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:]
+ else:
+ set = 1
+
+ # look up the value
+ try:
+ item = linkcl.lookup(item)
+ except KeyError, message:
+ 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:
+ newvalue.append(item)
+ if item not in curvalue:
+ curvalue.append(item)
+
+ # that's it, set the new Multilink property value,
+ # or overwrite it completely
+ if set:
+ props[propname] = newvalue
+ else:
+ props[propname] = curvalue
+ elif isinstance(proptype, hyperdb.Boolean):
+ value = value.strip()
+ props[propname] = value.lower() in ('yes', 'true', 'on', '1')
+ elif isinstance(proptype, hyperdb.Number):
+ value = value.strip()
+ props[propname] = int(value)
+
+ # 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
+Subject was: "%s"
+'''%(errors, subject)
+
+ #
# handle the users
- author = self.db.uidFromAddress(message.getaddrlist('from')[0])
+ #
+
+ # Don't create users if anonymous isn't allowed to register
+ create = 1
+ anonid = self.db.user.lookup('anonymous')
+ if not self.db.security.hasPermission('Email Registration', anonid):
+ create = 0
+
+ # ok, now figure out who the author is - create a new user if the
+ # "create" flag is true
+ author = uidFromAddress(self.db, message.getaddrlist('from')[0],
+ create=create)
+
+ # no author? means we're not author
+ if not author:
+ 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.'
+
+ # the author may have been created - make sure the change is
+ # committed before we reopen the database
+ self.db.commit()
+
+ # reopen the database as the author
+ username = self.db.user.get(author, 'username')
+ self.db = self.instance.open(username)
+
+ # re-get the class with the new database connection
+ cl = self.db.getclass(classname)
+
+ # now update the recipients list
recipients = []
+ tracker_email = self.instance.config.TRACKER_EMAIL.lower()
for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
- if recipient[1].strip().lower() == self.ISSUE_TRACKER_EMAIL:
+ 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 = uidFromAddress(self.db, recipient, create)
+
+ # if all's well, add the recipient to the list
+ if recipient:
+ recipients.append(recipient)
+
+ #
+ # handle message-id and in-reply-to
+ #
+ messageid = message.getheader('message-id')
+ inreplyto = message.getheader('in-reply-to') or ''
+ # generate a messageid if there isn't one
+ if not messageid:
+ messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
+ classname, nodeid, self.instance.config.MAIL_DOMAIN)
+
+ #
# now handle the body - find the message
+ #
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
- data = part.fp.read()
- encoding = part.getencoding()
- if encoding == 'base64':
- data = binascii.a2b_base64(data)
- elif encoding == 'quoted-printable':
- data = quopri.decode(data)
- elif encoding == 'uuencoded':
- data = binascii.a2b_uu(data)
+ data = self.get_part_data_decoded(part)
attachments.append((name, part.gettype(), data))
-
if content is None:
- raise ValueError, 'No text/plain part found'
+ raise MailUsageError, '''
+Roundup requires the submission to be plain text. The message parser could
+not find a text/plain part to use.
+'''
elif content_type[:10] == 'multipart/':
# skip over the intro to the first boundary
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 ValueError, 'No text/plain part found'
+ raise MailUsageError, '''
+Roundup requires the submission to be plain text. The message parser could
+not find a text/plain part to use.
+'''
elif content_type != 'text/plain':
- raise ValueError, 'No text/plain part found'
+ raise MailUsageError, '''
+Roundup requires the submission to be plain text. The message parser could
+not find a text/plain part to use.
+'''
else:
- content = message.fp.read()
+ 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'
- summary, content = parseContent(content)
+ # parse the body of the message, stripping out bits as appropriate
+ summary, content = parseContent(content, keep_citations,
+ keep_body)
- # handle the files
+ #
+ # handle the attachments
+ #
files = []
- for (name, type, data) in attachments:
- files.append(self.db.file.create(type=type, name=name,
+ for (name, mime_type, data) in attachments:
+ if not name:
+ name = "unnamed"
+ files.append(self.db.file.create(type=mime_type, name=name,
content=data))
- # 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.
+ #
+ # create the message if there's a message body (content)
+ #
+ if content:
message_id = self.db.msg.create(author=author,
recipients=recipients, date=date.Date('.'), summary=summary,
- content=content, files=files)
- messages = cl.get(nodeid, 'messages')
- messages.append(message_id)
- props['messages'] = messages
- cl.set(nodeid, **props)
- 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)
- # fill out the properties with defaults where required
- if properties.has_key('assignedto') and \
- not props.has_key('assignedto'):
- props['assignedto'] = '1' # "admin"
- if properties.has_key('status') and not props.has_key('status'):
- props['status'] = '1' # "unread"
- if properties.has_key('title') and not props.has_key('title'):
- props['title'] = title
- props['messages'] = [message_id]
- props['nosy'] = recipients[:]
- props['nosy'].append(author)
- props['nosy'].sort()
- nodeid = cl.create(**props)
-
-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*$')):
+ content=content, files=files, messageid=messageid,
+ inreplyto=inreplyto)
+
+ # attach the message to the node
+ if nodeid:
+ # add the message to the node's list
+ messages = cl.get(nodeid, 'messages')
+ messages.append(message_id)
+ props['messages'] = messages
+ else:
+ # pre-load the messages list
+ props['messages'] = [message_id]
+
+ # set the title to the subject
+ if properties.has_key('title') and not props.has_key('title'):
+ props['title'] = title
+
+ #
+ # perform the node change / create
+ #
+ try:
+ if nodeid:
+ cl.set(nodeid, **props)
+ else:
+ nodeid = cl.create(**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()
+
+ return nodeid
+
+def extractUserFromList(userClass, users):
+ '''Given a list of users, try to extract the first non-anonymous user
+ and return that user, otherwise return None
+ '''
+ if len(users) > 1:
+ for user in users:
+ # make sure we don't match the anonymous or admin user
+ if userClass.get(user, 'username') in ('admin', 'anonymous'):
+ continue
+ # first valid match will do
+ return user
+ # well, I guess we have no choice
+ return user[0]
+ elif users:
+ return users[0]
+ return None
+
+def uidFromAddress(db, address, create=1):
+ ''' address is from the rfc822 module, and therefore is (name, addr)
+
+ user is created if they don't exist in the db already
+ '''
+ (realname, address) = address
+
+ # try a straight match of the address
+ user = extractUserFromList(db.user, db.user.stringFind(address=address))
+ if user is not None: return user
+
+ # 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},
+ [], [])
+ user = extractUserFromList(db.user, users)
+ if user is not None: return user
+
+ # try to match the username to the address (for local
+ # submissions where the address is empty)
+ user = extractUserFromList(db.user, db.user.stringFind(username=address))
+
+ # couldn't match address or username, so create a new user
+ if create:
+ return db.user.create(username=address, address=address,
+ realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES)
+ else:
+ return 0
+
+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
non-quoting section becomes the summary of the message.
'''
- sections = blank_line.split(content)
+ # strip off leading carriage-returns / newlines
+ i = 0
+ for i in range(len(content)):
+ if content[i] not in '\r\n':
+ break
+ if i > 0:
+ sections = blank_line.split(content[i:])
+ else:
+ sections = blank_line.split(content)
+
# extract out the summary from the message
summary = ''
l = []
for section in sections:
- section = section.strip()
+ #section = section.strip()
if not section:
continue
lines = eol.split(section)
- if lines[0] and lines[0][0] in '>|':
- continue
- if len(lines) > 1 and lines[1] and lines[1][0] in '>|':
- continue
+ if (lines[0] and lines[0][0] in '>|') or (len(lines) > 1 and
+ lines[1] and lines[1][0] in '>|'):
+ # 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 '>|':
+ break
+ else:
+ # 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:
+ # 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)
+
if not summary:
+ # if we don't have our summary yet use the first line of this
+ # section
summary = lines[0]
- l.append(section)
- continue
- if 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'.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.8 2001/08/05 07:06:07 richard
-# removed some print statements
-#
-# Revision 1.7 2001/08/03 07:18:22 richard
-# Implemented correct mail splitting (was taking a shortcut). Added unit
-# tests. Also snips signatures now too.
-#
-# Revision 1.6 2001/08/01 04:24:21 richard
-# mailgw was assuming certain properties existed on the issues being created.
-#
-# Revision 1.5 2001/07/29 07:01:39 richard
-# Added vim command to all source so that we don't get no steenkin' tabs :)
-#
-# Revision 1.4 2001/07/28 06:43:02 richard
-# Multipart message class has the getPart method now. Added some tests for it.
-#
-# Revision 1.3 2001/07/28 00:34:34 richard
-# Fixed some non-string node ids.
-#
-# Revision 1.2 2001/07/22 12:09:32 richard
-# Final commit of Grande Splite
-#
-#
# vim: set filetype=python ts=4 sw=4 et si