X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fmailgw.py;h=f45eedc9d51af4e3e6e0a88259c054676215dcdb;hb=c2760e668d93a49bcc08a62bc0ef8452986b8886;hp=f71e63fbbc049d28d2a53e8fb8d3e5503cebd8ad;hpb=532f94f822feb1cc66a4150705ec394f83c19fdc;p=roundup.git diff --git a/roundup/mailgw.py b/roundup/mailgw.py index f71e63f..f45eedc 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -16,8 +16,7 @@ # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -''' -An e-mail gateway for Roundup. +"""An e-mail gateway for Roundup. Incoming messages are examined for multiple parts: . In a multipart/mixed message or part, each subpart is extracted and @@ -73,14 +72,16 @@ are calling the create() method to create a new node). If an auditor raises an exception, the original message is bounced back to the sender with the explanatory message given in the exception. -$Id: mailgw.py,v 1.126 2003-06-25 08:02:51 neaj Exp $ -''' +$Id: mailgw.py,v 1.143 2004-02-11 23:55:08 richard Exp $ +""" +__docformat__ = 'restructuredtext' import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri import time, random, sys import traceback, MimeWriter, rfc822 from roundup import hyperdb, date, password, rfc2822 +from roundup.mailer import Mailer SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '') @@ -91,14 +92,22 @@ class MailUsageError(ValueError): pass class MailUsageHelp(Exception): - pass - -class MailLoop(Exception): - ''' We've seen this message before... ''' + """ We need to send the help message to the user. """ pass class Unauthorized(Exception): """ Access denied """ + pass + +class IgnoreMessage(Exception): + """ A general class of message that we should ignore. """ + pass +class IgnoreBulk(IgnoreMessage): + """ This is email from a mailing list or from a vacation program. """ + pass +class IgnoreLoop(IgnoreMessage): + """ We've seen this message before... """ + pass def initialiseSecurity(security): ''' Create some Permissions and Roles on the security object @@ -132,40 +141,11 @@ def getparam(str, param): return rfc822.unquote(f[i+1:].strip()) return None -def openSMTPConnection(config): - ''' Open an SMTP connection to the mailhost specified in the config - ''' - smtp = smtplib.SMTP(config.MAILHOST) - - # use TLS? - use_tls = getattr(config, 'MAILHOST_TLS', 'no') - if use_tls == 'yes': - # do we have key files too? - keyfile = getattr(config, 'MAILHOST_TLS_KEYFILE', '') - if keyfile: - certfile = getattr(config, 'MAILHOST_TLS_CERTFILE', '') - if certfile: - args = (keyfile, certfile) - else: - args = (keyfile, ) - else: - args = () - # start the TLS - smtp.starttls(*args) - - # ok, now do we also need to log in? - mailuser = getattr(config, 'MAILUSER', None) - if mailuser: - smtp.login(*config.MAILUSER) - - # that's it, a fully-configured SMTP connection ready to go - return smtp - class Message(mimetools.Message): ''' subclass mimetools.Message so we can retrieve the parts of the message... ''' - def getPart(self): + def getpart(self): ''' Get a single part of a multipart message and return it as a new Message instance. ''' @@ -184,12 +164,136 @@ class Message(mimetools.Message): s.seek(0) return Message(s) + def getparts(self): + """Get all parts of this multipart message.""" + # skip over the intro to the first boundary + self.getpart() + + # accumulate the other parts + parts = [] + while 1: + part = self.getpart() + if part is None: + break + parts.append(part) + return parts + def getheader(self, name, default=None): hdr = mimetools.Message.getheader(self, name, default) if hdr: hdr = hdr.replace('\n','') # Inserted by rfc822.readheaders return rfc2822.decode_header(hdr) - + + def getname(self): + """Find an appropriate name for this message.""" + if self.gettype() == 'message/rfc822': + # handle message/rfc822 specially - the name should be + # the subject of the actual e-mail embedded here + self.fp.seek(0) + name = Message(self.fp).getheader('subject') + else: + # try name on Content-Type + name = self.getparam('name') + if not name: + disp = self.getheader('content-disposition', None) + if disp: + name = getparam(disp, 'filename') + + if name: + return name.strip() + + def getbody(self): + """Get the decoded message body.""" + self.rewindbody() + encoding = self.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(self.fp.read()) + elif encoding == 'quoted-printable': + # the quopri module wants to work with files + decoded = cStringIO.StringIO() + quopri.decode(self.fp, decoded) + data = decoded.getvalue() + elif encoding == 'uuencoded': + data = binascii.a2b_uu(self.fp.read()) + else: + # take it as text + data = self.fp.read() + + # Encode message to unicode + charset = rfc2822.unaliasCharset(self.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 + + # 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. + + def extract_content(self, parent_type=None): + """Extract the body and the attachments recursively.""" + content_type = self.gettype() + content = None + attachments = [] + + if content_type == 'text/plain': + content = self.getbody() + elif content_type[:10] == 'multipart/': + for part in self.getparts(): + new_content, new_attach = part.extract_content(content_type) + + # If we haven't found a text/plain part yet, take this one, + # otherwise make it an attachment. + if not content: + content = new_content + elif new_content: + attachments.append(part.as_attachment()) + + attachments.extend(new_attach) + elif (parent_type == 'multipart/signed' and + content_type == 'application/pgp-signature'): + # ignore it so it won't be saved as an attachment + pass + else: + attachments.append(self.as_attachment()) + return content, attachments + + def as_attachment(self): + """Return this message as an attachment.""" + return (self.getname(), self.gettype(), self.getbody()) + class MailGW: # Matches subjects like: @@ -209,19 +313,20 @@ class MailGW: self.instance = instance self.db = db self.arguments = arguments + self.mailer = Mailer(instance.config) # 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 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) @@ -229,9 +334,9 @@ class MailGW: return 0 def do_mailbox(self, filename): - ''' Read a series of messages from the specified unix mailbox file and + """ 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 is deprecated in py2.3 and fcntl takes over all the symbols @@ -272,7 +377,7 @@ class MailGW: import getpass, poplib, socket try: if not user: - user = raw_input(_('User: ')) + user = raw_input('User: ') if not password: password = getpass.getpass() except (KeyboardInterrupt, EOFError): @@ -314,64 +419,20 @@ class MailGW: return self.handle_Message(Message(fp)) def handle_Message(self, message): - '''Handle an RFC822 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. - ''' + """ # 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: - 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 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.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: + sendto = message.getaddrlist('resent-from') + if not sendto: + sendto = message.getaddrlist('from') + if not sendto: # 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] @@ -381,94 +442,58 @@ class MailGW: 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, + self.mailer.bounce_message(message, sendto, m, subject='Badly formed message from mail gateway') + return - # now send the message - if SENDMAILDEBUG: - open(SENDMAILDEBUG, 'a').write('From: %s\nTo: %s\n%s\n'%( - self.instance.config.ADMIN_EMAIL, ', '.join(sendto), - m.getvalue())) - else: - try: - smtp = openSMTPConnection(self.instance.config) - smtp.sendmail(self.instance.config.ADMIN_EMAIL, sendto, - m.getvalue()) - except socket.error, value: - raise MailGWError, "Couldn't send error email: "\ - "mailhost %s"%value - except smtplib.SMTPException, 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('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; charset=utf-8') - body.write('\n'.join(error)) - - # 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: - body.write(header) - body.write('\n') + # try normal message-handling + if not self.trapExceptions: + return self.handle_message(message) try: - message.rewindbody() - except IOError, message: - body.write("*** couldn't include message body: %s ***"%message) - else: - body.write(message.fp.read()) - - 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() - - # 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 + 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) + self.mailer.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) + self.mailer.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)) + self.mailer.bounce_message(message, sendto, m) + except IgnoreMessage: + # XXX we should use a log file here... + # do not take any action + # this exception is thrown when email should be ignored + 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.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()) + self.mailer.bounce_message(message, sendto, m) def handle_message(self, message): ''' message - a Message instance @@ -477,7 +502,11 @@ class MailGW: ''' # detect loops if message.getheader('x-roundup-loop', ''): - raise MailLoop + raise IgnoreLoop + + # detect Precedence: Bulk + if (message.getheader('precedence', '') == 'bulk'): + raise IgnoreBulk # XXX Don't enable. This doesn't work yet. # "[^A-z.]tracker\+(?P[^\d\s]+)(?P\d+)\@some.dom.ain[^A-z.]" @@ -496,6 +525,11 @@ class MailGW: # nodeid = issue.group('nodeid') # break + # determine the sender's address + from_list = message.getaddrlist('resent-from') + if not from_list: + from_list = message.getaddrlist('from') + # handle the subject line subject = message.getheader('subject', '') @@ -514,28 +548,38 @@ Emails to Roundup trackers must include a Subject: line! # 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: + # no classname, check if this a registration confirmation email + # or fallback on the default class + otk_re = re.compile('-- key (?P[a-zA-Z0-9]{32})') + otk = otk_re.search(m.group('title')) + if otk: + self.db.confirm_registration(otk.group('otk')) + subject = 'Your registration to %s is complete' % \ + self.instance.config.TRACKER_NAME + sendto = [from_list[0][1]] + self.mailer.standard_message(sendto, subject, '') + return + elif 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 MailUsageError, ''' + 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: +'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". + - 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 +Subject was: '%s' +"""%subject # get the class try: @@ -646,8 +690,7 @@ The mail gateway is not properly set up. Please contact # 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) + author = uidFromAddress(self.db, from_list[0], create=create) # if we're not recognised, and we don't get added as a user, then we # must be anonymous @@ -662,7 +705,7 @@ The mail gateway is not properly set up. Please contact You are not a registered user. Unknown address: %s -'''%message.getaddrlist('from')[0][1] +'''%from_list[0][1] else: # we're registered and we're _still_ not allowed access raise Unauthorized, 'You are not permitted to access '\ @@ -711,7 +754,7 @@ Unknown address: %s errors, props = setPropArrayFromString(self, cl, args, nodeid) # handle any errors parsing the argument list if errors: - errors = '\n- '.join(errors) + errors = '\n- '.join(map(str, errors)) raise MailUsageError, ''' There were problems handling your subject line argument list: - %s @@ -734,118 +777,13 @@ Subject was: "%s" 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() - content = None - while 1: - # get the next part - part = message.getPart() - if part is None: - break - # parse it - subtype = part.gettype() - if subtype == 'text/plain' and not content: - # 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 - i = part.fp.tell() - mailmess = Message(part.fp) - 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') - 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)) - if content is None: - 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 - message.getPart() - content = None - while 1: - # get the next part - part = message.getPart() - if part is None: - break - # parse it - if part.gettype() == 'text/plain' and not content: - 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 -not find a text/plain part to use. -''' - - elif content_type != 'text/plain': + content, attachments = message.extract_content() + if content is None: raise MailUsageError, ''' Roundup requires the submission to be plain text. The message parser could not find a text/plain part to use. ''' - - else: - content = self.get_part_data_decoded(message) # figure how much we should muck around with the email body keep_citations = getattr(self.instance.config, 'EMAIL_KEEP_QUOTED_TEXT', @@ -861,27 +799,27 @@ not find a text/plain part to use. # # handle the attachments # - files = [] - 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, **file_props)) - # attach the files to the issue - if nodeid: - # extend the existing files list - fileprop = cl.get(nodeid, 'files') - fileprop.extend(files) - props['files'] = fileprop - else: - # pre-load the files list - props['files'] = files - + if properties.has_key('files'): + files = [] + 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, **file_props)) + # attach the files to the issue + if nodeid: + # extend the existing files list + fileprop = cl.get(nodeid, 'files') + fileprop.extend(files) + props['files'] = fileprop + else: + # pre-load the files list + props['files'] = files # # create the message if there's a message body (content) # - if content: + if (content and properties.has_key('messages')): message_id = self.db.msg.create(author=author, recipients=recipients, date=date.Date('.'), summary=summary, content=content, files=files, messageid=messageid, @@ -923,11 +861,10 @@ There was a problem with the message you sent: return nodeid -def setPropArrayFromString(self, cl, propString, nodeid = None): +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, ';'): @@ -938,100 +875,13 @@ def setPropArrayFromString(self, cl, propString, nodeid = None): errors.append('not of form [arg=value,value,...;' 'arg=value,value,...]') return (errors, props) - - # ensure it's a valid property name + # convert the value to a hyperdb-usable value 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. - # =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) + props[propname] = hyperdb.rawToHyperdb(self.db, cl, nodeid, + propname, value) + except hyperdb.HyperdbValueError, message: + errors.append(message) return errors, props @@ -1108,7 +958,7 @@ def uidFromAddress(db, address, create=1, **user_props): 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*$'), + signature=re.compile(r'^[>|\s]*-- ?$'), original_msg=re.compile(r'^[>|\s]*-----\s?Original Message\s?-----$')): ''' The message body is divided into sections by blank lines. Sections where the second and all subsequent lines begin with a ">"