diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index 07a041d63ced5e28e723bc08bb1c795536da26d8..94594de1a938d88703409e2c841be2f9998ba8c5 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-__doc__ = '''
+'''
An e-mail gateway for Roundup.
Incoming messages are examined for multiple parts:
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.
an exception, the original message is bounced back to the sender with the
explanatory message given in the exception.
-$Id: mailgw.py,v 1.86 2002-09-10 12:44:42 richard Exp $
+$Id: mailgw.py,v 1.113 2003-03-24 02:54:35 richard Exp $
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
'''
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
-import time, random
-import traceback, MimeWriter
+import time, random, sys
+import traceback, MimeWriter, rfc822
import hyperdb, date, password
import hyperdb, date, password
+import rfc2822
+
SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
class MailGWError(ValueError):
SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
class MailGWError(ValueError):
class MailUsageHelp(Exception):
pass
class MailUsageHelp(Exception):
pass
+class MailLoop(Exception):
+ ''' We've seen this message before... '''
+ pass
+
class Unauthorized(Exception):
""" Access denied """
class Unauthorized(Exception):
""" Access denied """
description="User may use the email interface")
security.addPermissionToRole('Admin', p)
description="User may use the email interface")
security.addPermissionToRole('Admin', p)
+def getparam(str, param):
+ ''' From the rfc822 "header" string, extract "param" if it appears.
+ '''
+ if ';' not in str:
+ return None
+ str = str[str.index(';'):]
+ while str[:1] == ';':
+ str = str[1:]
+ if ';' in str:
+ # XXX Should parse quotes!
+ end = str.index(';')
+ else:
+ end = len(str)
+ f = str[:end]
+ if '=' in f:
+ i = f.index('=')
+ if f[:i].strip().lower() == param:
+ return rfc822.unquote(f[i+1:].strip())
+ return None
+
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
message...
s.seek(0)
return Message(s)
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)
+ def getheader(self, name, default=None):
+ hdr = mimetools.Message.getheader(self, name, default)
+ return rfc2822.decode_header(hdr)
+
+subject_re = re.compile(r'(?P<refwd>\s*\W?\s*(fw|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:
class MailGW:
- def __init__(self, instance, db):
+ def __init__(self, instance, db, arguments={}):
self.instance = instance
self.db = db
self.instance = instance
self.db = db
+ self.arguments = arguments
# should we trap exceptions (normal usage) or pass them through
# (for testing)
self.trapExceptions = 1
# should we trap exceptions (normal usage) or pass them through
# (for testing)
self.trapExceptions = 1
+ 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...
+ '''
+ s = cStringIO.StringIO()
+ s.write(sys.stdin.read())
+ s.seek(0)
+ self.main(s)
+ return 0
+
+ def do_mailbox(self, filename):
+ ''' Read a series of messages from the specified unix mailbox file and
+ pass each to the mail handler.
+ '''
+ # open the spool file and lock it
+ import fcntl, FCNTL
+ f = open(filename, 'r+')
+ fcntl.flock(f.fileno(), FCNTL.LOCK_EX)
+
+ # handle and clear the mailbox
+ try:
+ from mailbox import UnixMailbox
+ mailbox = UnixMailbox(f, factory=Message)
+ # grab one message
+ message = mailbox.next()
+ while message:
+ # handle this message
+ self.handle_Message(message)
+ message = mailbox.next()
+ # nuke the file contents
+ os.ftruncate(f.fileno(), 0)
+ except:
+ import traceback
+ traceback.print_exc()
+ return 1
+ fcntl.flock(f.fileno(), FCNTL.LOCK_UN)
+ return 0
+
+ def do_apop(self, server, user='', password=''):
+ ''' Do authentication POP
+ '''
+ self.do_pop(server, user, password, apop=1)
+
+ def do_pop(self, server, user='', password='', apop=0):
+ '''Read a series of messages from the specified POP server.
+ '''
+ import getpass, poplib, socket
+ try:
+ if not user:
+ user = raw_input(_('User: '))
+ if not password:
+ password = getpass.getpass()
+ except (KeyboardInterrupt, EOFError):
+ # Ctrl C or D maybe also Ctrl Z under Windows.
+ print "\nAborted by user."
+ return 1
+
+ # open a connection to the server and retrieve all messages
+ try:
+ server = poplib.POP3(server)
+ except socket.error, message:
+ print "POP server error:", message
+ return 1
+ if apop:
+ server.apop(user, password)
+ else:
+ server.user(user)
+ server.pass_(password)
+ numMessages = len(server.list()[1])
+ for i in range(1, numMessages+1):
+ # retr: returns
+ # [ pop response e.g. '+OK 459 octets',
+ # [ array of message lines ],
+ # number of octets ]
+ lines = server.retr(i)[1]
+ s = cStringIO.StringIO('\n'.join(lines))
+ s.seek(0)
+ self.handle_Message(Message(s))
+ # delete the message
+ server.dele(i)
+
+ # quit the server to commit changes.
+ server.quit()
+ return 0
+
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.
'''
m = ['']
m.append(str(value))
m = self.bounce_message(message, sendto, m)
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
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')
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
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('')
sendto = [self.instance.config.ADMIN_EMAIL]
m = ['Subject: badly formed message from mail gateway']
m.append('')
# now send the message
if SENDMAILDEBUG:
# now send the message
if SENDMAILDEBUG:
- open(SENDMAILDEBUG, 'w').write('From: %s\nTo: %s\n%s\n'%(
+ open(SENDMAILDEBUG, 'a').write('From: %s\nTo: %s\n%s\n'%(
self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
m.getvalue()))
else:
self.instance.config.ADMIN_EMAIL, ', '.join(sendto),
m.getvalue()))
else:
'''
msg = cStringIO.StringIO()
writer = MimeWriter.MimeWriter(msg)
'''
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('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()
writer.addheader('MIME-Version', '1.0')
part = writer.startmultipartbody('mixed')
part = writer.nextpart()
- body = part.startbody('text/plain')
+ body = part.startbody('text/plain; charset=utf-8')
body.write('\n'.join(error))
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:
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()
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())
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
writer.lastpart()
return msg
else:
# take it as text
data = part.fp.read()
else:
# take it as text
data = part.fp.read()
- return data
+
+ # Encode message to unicode
+ charset = rfc2822.unaliasCharset(part.getparam("charset"))
+ if charset:
+ # Do conversion only if charset specified
+ edata = unicode(data, charset).encode('utf-8')
+ # Convert from dos eol to unix
+ edata = edata.replace('\r\n', '\n')
+ else:
+ # Leave message content as is
+ edata = data
+
+ return edata
def handle_message(self, message):
''' message - a Message instance
Parse the message as per the module docstring.
'''
def handle_message(self, message):
''' message - a Message instance
Parse the message as per the module docstring.
'''
+ # detect loops
+ if message.getheader('x-roundup-loop', ''):
+ raise MailLoop
+
+ # XXX Don't enable. This doesn't work yet.
+# "[^A-z.]tracker\+(?P<classname>[^\d\s]+)(?P<nodeid>\d+)\@some.dom.ain[^A-z.]"
+ # handle delivery to addresses like:tracker+issue25@some.dom.ain
+ # use the embedded issue number as our issue
+# if hasattr(self.instance.config, 'EMAIL_ISSUE_ADDRESS_RE') and \
+# self.instance.config.EMAIL_ISSUE_ADDRESS_RE:
+# issue_re = self.instance.config.EMAIL_ISSUE_ADDRESS_RE
+# for header in ['to', 'cc', 'bcc']:
+# addresses = message.getheader(header, '')
+# if addresses:
+# # FIXME, this only finds the first match in the addresses.
+# issue = re.search(issue_re, addresses, 'i')
+# if issue:
+# classname = issue.group('classname')
+# nodeid = issue.group('nodeid')
+# break
+
# handle the subject line
subject = message.getheader('subject', '')
# handle the subject line
subject = message.getheader('subject', '')
- if subject.strip() == 'help':
+ if subject.strip().lower() == 'help':
raise MailUsageHelp
m = subject_re.match(subject)
raise MailUsageHelp
m = subject_re.match(subject)
else:
title = ''
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, '''
# but we do need either a title or a nodeid...
if nodeid is None and not title:
raise MailUsageError, '''
Subject was: "%s"
'''%(nodeid, subject)
Subject was: "%s"
'''%(nodeid, subject)
+
+ # Handle the arguments specified by the email gateway command line.
+ # We do this by looping over the list of self.arguments looking for
+ # a -C to tell us what class then the -S setting string.
+ msg_props = {}
+ user_props = {}
+ file_props = {}
+ issue_props = {}
+ # so, if we have any arguments, use them
+ if self.arguments:
+ current_class = 'msg'
+ for option, propstring in self.arguments:
+ if option in ( '-C', '--class'):
+ current_class = propstring.strip()
+ if current_class not in ('msg', 'file', 'user', 'issue'):
+ raise MailUsageError, '''
+The mail gateway is not properly set up. Please contact
+%s and have them fix the incorrect class specified as:
+ %s
+'''%(self.instance.config.ADMIN_EMAIL, current_class)
+ if option in ('-S', '--set'):
+ if current_class == 'issue' :
+ errors, issue_props = setPropArrayFromString(self,
+ cl, propstring.strip(), nodeid)
+ elif current_class == 'file' :
+ temp_cl = self.db.getclass('file')
+ errors, file_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ elif current_class == 'msg' :
+ temp_cl = self.db.getclass('msg')
+ errors, msg_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ elif current_class == 'user' :
+ temp_cl = self.db.getclass('user')
+ errors, user_props = setPropArrayFromString(self,
+ temp_cl, propstring.strip())
+ if errors:
+ raise MailUsageError, '''
+The mail gateway is not properly set up. Please contact
+%s and have them fix the incorrect properties:
+ %s
+'''%(self.instance.config.ADMIN_EMAIL, errors)
+
#
# handle the users
#
#
# handle the users
#
author = uidFromAddress(self.db, message.getaddrlist('from')[0],
create=create)
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:
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]
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):
# make sure they're allowed to edit this class of information
if not self.db.security.hasPermission('Edit', author, classname):
# reopen the database as the author
username = self.db.user.get(author, 'username')
# reopen the database as the author
username = self.db.user.get(author, 'username')
+ self.db.close()
self.db = self.instance.open(username)
# re-get the class with the new database connection
self.db = self.instance.open(username)
# re-get the class with the new database connection
# look up the recipient - create if necessary (and we're
# allowed to)
# look up the recipient - create if necessary (and we're
# allowed to)
- recipient = uidFromAddress(self.db, recipient, create)
+ recipient = uidFromAddress(self.db, recipient, create, **user_props)
# if all's well, add the recipient to the list
if recipient:
recipients.append(recipient)
#
# if all's well, add the recipient to the list
if recipient:
recipients.append(recipient)
#
- # extract the args
+ # XXX extract the args NOT USED WHY -- rouilj
#
subject_args = m.group('args')
#
subject_args = m.group('args')
props = {}
args = m.group('args')
if args:
props = {}
args = m.group('args')
if args:
- errors = []
- for prop in string.split(args, ';'):
- # extract the property name and value
- try:
- propname, value = prop.split('=')
- except ValueError, message:
- 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)
-
+ errors, props = setPropArrayFromString(self, cl, args, nodeid)
# handle any errors parsing the argument list
if errors:
errors = '\n- '.join(errors)
# handle any errors parsing the argument list
if errors:
errors = '\n- '.join(errors)
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()))
+ elif subtype == 'multipart/alternative':
+ # Search for text/plain in message with attachment and
+ # alternative text representation
+ # skip over intro to first boundary
+ part.getPart()
+ while 1:
+ # get the next part
+ subpart = part.getPart()
+ if subpart is None:
+ break
+ # parse it
+ if subpart.gettype() == 'text/plain' and not content:
+ content = self.get_part_data_decoded(subpart)
else:
# try name on Content-Type
name = part.getparam('name')
else:
# try name on Content-Type
name = part.getparam('name')
+ if name:
+ name = name.strip()
+ if not name:
+ disp = part.getheader('content-disposition', None)
+ if disp:
+ name = getparam(disp, 'filename')
+ if name:
+ name = name.strip()
# this is just an attachment
data = self.get_part_data_decoded(part)
attachments.append((name, part.gettype(), data))
# 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
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'
'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
'no') == 'yes'
# parse the body of the message, stripping out bits as appropriate
if not name:
name = "unnamed"
files.append(self.db.file.create(type=mime_type, name=name,
if not name:
name = "unnamed"
files.append(self.db.file.create(type=mime_type, name=name,
- content=data))
+ content=data, **file_props))
#
# create the message if there's a message body (content)
#
# create the message if there's a message body (content)
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,
- inreplyto=inreplyto)
+ inreplyto=inreplyto, **msg_props)
# attach the message to the node
if nodeid:
# attach the message to the node
if nodeid:
# perform the node change / create
#
try:
# perform the node change / create
#
try:
+ # merge the command line props defined in issue_props into
+ # the props dictionary because function(**props, **issue_props)
+ # is a syntax error.
+ for prop in issue_props.keys() :
+ if not props.has_key(prop) :
+ props[prop] = issue_props[prop]
if nodeid:
cl.set(nodeid, **props)
else:
if nodeid:
cl.set(nodeid, **props)
else:
return nodeid
return nodeid
+
+def setPropArrayFromString(self, cl, propString, nodeid = None):
+ ''' takes string of form prop=value,value;prop2=value
+ and returns (error, prop[..])
+ '''
+ properties = cl.getprops()
+ props = {}
+ errors = []
+ for prop in string.split(propString, ';'):
+ # extract the property name and value
+ try:
+ propname, value = prop.split('=')
+ except ValueError, message:
+ errors.append('not of form [arg=value,value,...;'
+ 'arg=value,value,...]')
+ return (errors, props)
+
+ # 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()).local(self.db.getUserTimezone())
+ 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] = float(value)
+ return errors, props
+
+
def extractUserFromList(userClass, users):
'''Given a list of users, try to extract the first non-anonymous user
and return that user, otherwise return None
def extractUserFromList(userClass, users):
'''Given a list of users, try to extract the first non-anonymous user
and return that user, otherwise return None
return users[0]
return None
return users[0]
return None
-def uidFromAddress(db, address, create=1):
+
+def uidFromAddress(db, address, create=1, **user_props):
''' address is from the rfc822 module, and therefore is (name, addr)
user is created if they don't exist in the db already
''' address is from the rfc822 module, and therefore is (name, addr)
user is created if they don't exist in the db already
+ user_props may supply additional user information
'''
(realname, address) = address
# try a straight match of the address
user = extractUserFromList(db.user, db.user.stringFind(address=address))
'''
(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
+ if user is not None:
+ return user
# try the user alternate addresses if possible
props = db.user.getprops()
if props.has_key('alternate_addresses'):
# 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)
user = extractUserFromList(db.user, users)
- if user is not None: return user
+ if user is not None:
+ return user
# try to match the username to the address (for local
# submissions where the address is empty)
# try to match the username to the address (for local
# submissions where the address is empty)
# couldn't match address or username, so create a new user
if create:
return db.user.create(username=address, address=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)
+ realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES,
+ **user_props)
else:
return 0
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*$'),
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-----$')):
+ original_msg=re.compile(r'^[>|\s]*-----\s?Original Message\s?-----$')):
''' The message body is divided into sections by blank lines.
''' 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 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.
+
+ If keep_citations is true, then we keep the "quoting sections" in the
+ content.
+ If keep_body is true, we even keep the signature sections.
'''
# strip off leading carriage-returns / newlines
i = 0
'''
# strip off leading carriage-returns / newlines
i = 0
# see if there's a response somewhere inside this section (ie.
# no blank line between quoted message and response)
for line in lines[1:]:
# 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
break
else:
# we keep quoted bits if specified in the config
l.append(section)
continue
# keep this section - it has reponse stuff in it
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)
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
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
elif signature.match(lines[0]) and 2 <= len(lines) <= 10:
# lose any signature
break
- elif original_message.match(lines[0]):
+ elif original_msg.match(lines[0]):
# ditch the stupid Outlook quoting of the entire original message
break
# and add the section to the output
l.append(section)
# ditch the stupid Outlook quoting of the entire original message
break
# and add the section to the output
l.append(section)
- # we only set content for those who want to delete cruft from the
- # message body, otherwise the body is left untouched.
+
+ # 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:
content = '\n\n'.join(l)
if not keep_body:
content = '\n\n'.join(l)
+
return summary, content
# vim: set filetype=python ts=4 sw=4 et si
return summary, content
# vim: set filetype=python ts=4 sw=4 et si