X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fmailgw.py;h=d269554f8783216a05f51de73ed062c15fa28ca3;hb=7dfd71fda6f22919702dabcfdda3d763d9b5d350;hp=922dc12656a8d6dafc601aba82223b97fad1a32c;hpb=1631bfec580afbc7c7051a8c0e169297092304ba;p=roundup.git diff --git a/roundup/mailgw.py b/roundup/mailgw.py index 922dc12..d269554 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -92,7 +92,7 @@ from roundup.i18n import _ from roundup.hyperdb import iter_roles try: - import pyme, pyme.core, pyme.gpgme + import pyme, pyme.core, pyme.constants, pyme.constants.sigsum except ImportError: pyme = None @@ -156,39 +156,31 @@ def gpgh_key_getall(key, attr): ''' return list of given attribute for all uids in a key ''' - u = key.uids - while u: + for u in key.uids: yield getattr(u, attr) - u = u.next -def gpgh_sigs(sig): - ''' more pythonic iteration over GPG signatures ''' - while sig: - yield sig - sig = sig.next - -def check_pgp_sigs(sig, gpgctx, author): +def check_pgp_sigs(sigs, gpgctx, author): ''' Theoretically a PGP message can have several signatures. GPGME - returns status on all signatures in a linked list. Walk that - linked list looking for the author's signature + returns status on all signatures in a list. Walk that list + looking for the author's signature ''' - for sig in gpgh_sigs(sig): + for sig in sigs: key = gpgctx.get_key(sig.fpr, False) # we really only care about the signature of the user who # submitted the email if key and (author in gpgh_key_getall(key, 'email')): - if sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID: + if sig.summary & pyme.constants.sigsum.VALID: return True else: # try to narrow down the actual problem to give a more useful # message in our bounce - if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING: + if sig.summary & pyme.constants.sigsum.KEY_MISSING: raise MailUsageError, \ _("Message signed with unknown key: %s") % sig.fpr - elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED: + elif sig.summary & pyme.constants.sigsum.KEY_EXPIRED: raise MailUsageError, \ _("Message signed with an expired key: %s") % sig.fpr - elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED: + elif sig.summary & pyme.constants.sigsum.KEY_REVOKED: raise MailUsageError, \ _("Message signed with a revoked key: %s") % sig.fpr else: @@ -247,6 +239,22 @@ class Message(mimetools.Message): parts.append(part) return parts + def _decode_header_to_utf8(self, hdr): + l = [] + prev_encoded = False + for part, encoding in decode_header(hdr): + if encoding: + part = part.decode(encoding) + # RFC 2047 specifies that between encoded parts spaces are + # swallowed while at the borders from encoded to non-encoded + # or vice-versa we must preserve a space. Multiple adjacent + # non-encoded parts should not occur. + if l and prev_encoded != bool(encoding): + l.append(' ') + prev_encoded = bool(encoding) + l.append(part) + return ''.join([s.encode('utf-8') for s in l]) + def getheader(self, name, default=None): hdr = mimetools.Message.getheader(self, name, default) # TODO are there any other False values possible? @@ -257,24 +265,13 @@ class Message(mimetools.Message): return '' if hdr: hdr = hdr.replace('\n','') # Inserted by rfc822.readheaders - # historically this method has returned utf-8 encoded string - l = [] - for part, encoding in decode_header(hdr): - if encoding: - part = part.decode(encoding) - l.append(part) - return ''.join([s.encode('utf-8') for s in l]) + return self._decode_header_to_utf8(hdr) def getaddrlist(self, name): # overload to decode the name part of the address l = [] for (name, addr) in mimetools.Message.getaddrlist(self, name): - p = [] - for part, encoding in decode_header(name): - if encoding: - part = part.decode(encoding) - p.append(part) - name = ''.join([s.encode('utf-8') for s in p]) + name = self._decode_header_to_utf8(name) l.append((name, addr)) return l @@ -506,7 +503,6 @@ class Message(mimetools.Message): raise MailUsageError, \ _("No PGP signature found in message.") - context = pyme.core.Context() # msg.getbody() is skipping over some headers that are # required to be present for verification to succeed so # we'll do this by hand @@ -521,6 +517,7 @@ class Message(mimetools.Message): msg_data = pyme.core.Data(canonical_msg) sig_data = pyme.core.Data(sig.getbody()) + context = pyme.core.Context() context.op_verify(sig_data, msg_data, None) # check all signatures for validity @@ -549,6 +546,7 @@ class parsedMessage: self.nodeid = None self.author = None self.recipients = None + self.msg_props = {} self.props = None self.content = None self.attachments = None @@ -856,7 +854,7 @@ Unknown address: %(from_address)s 'You are not permitted to access this tracker.') self.author = author - def check_node_permissions(self): + def check_permissions(self): ''' Check if the author has permission to edit or create this class of node ''' @@ -989,7 +987,7 @@ Subject was: "%(subject)s" """ if self.config.PGP_ROLES: return self.db.user.has_role(self.author, - iter_roles(self.config.PGP_ROLES)) + *iter_roles(self.config.PGP_ROLES)) else: return True @@ -1052,13 +1050,14 @@ encrypted.""") 'You are not permitted to add files to %(classname)s.' ) % self.__dict__ + self.msg_props['files'] = files if self.nodeid: # extend the existing files list fileprop = self.cl.get(self.nodeid, 'files') fileprop.extend(files) files = fileprop - self.props['files'] = files + self.props['files'] = files def create_msg(self): ''' Create msg containing all the relevant information from the message @@ -1066,6 +1065,7 @@ encrypted.""") if not self.properties.has_key('messages'): return msg_props = self.mailgw.get_class_arguments('msg') + self.msg_props.update (msg_props) # Get the message ids inreplyto = self.message.getheader('in-reply-to') or '' @@ -1093,8 +1093,8 @@ not find a text/plain part to use. try: message_id = self.db.msg.create(author=self.author, recipients=self.recipients, date=date.Date('.'), - summary=summary, content=content, files=self.props['files'], - messageid=messageid, inreplyto=inreplyto, **msg_props) + summary=summary, content=content, + messageid=messageid, inreplyto=inreplyto, **self.msg_props) except exceptions.Reject, error: raise MailUsageError, _(""" Mail message was rejected by a detector. @@ -1147,6 +1147,71 @@ There was a problem with the message you sent: return self.nodeid + # XXX Don't enable. This doesn't work yet. +# "[^A-z.]tracker\+(?P[^\d\s]+)(?P\d+)\@some.dom.ain[^A-z.]" + # handle delivery to addresses like:tracker+issue25@some.dom.ain + # use the embedded issue number as our issue +# issue_re = config['MAILGW_ISSUE_ADDRESS_RE'] +# if issue_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 + + # Default sequence of methods to be called on message. Use this for + # easier override of the default message processing + # list consists of tuples (method, return_if_true), the parsing + # returns if the return_if_true flag is set for a method *and* the + # method returns something that evaluates to True. + method_list = [ + # Filter out messages to ignore + (handle_ignore, False), + # Check for usage/help requests + (handle_help, False), + # Check if the subject line is valid + (check_subject, False), + # get importants parts from subject + (parse_subject, False), + # check for registration OTK + (rego_confirm, True), + # get the classname + (get_classname, False), + # get the optional nodeid: + (get_nodeid, False), + # Determine who the author is: + (get_author_id, False), + # allowed to edit or create this class? + (check_permissions, False), + # author may have been created: + # commit author to database and re-open as author + (commit_and_reopen_as_author, False), + # Get the recipients list + (get_recipients, False), + # get the new/updated node props + (get_props, False), + # Handle PGP signed or encrypted messages + (get_pgp_message, False), + # extract content and attachments from message body: + (get_content_and_attachments, False), + # put attachments into files linked to the issue: + (create_files, False), + # create the message if there's a message body (content): + (create_msg, False), + ] + + + def parse (self): + for method, flag in self.method_list: + ret = method(self) + if flag and ret: + return + # perform the node change / create: + return self.create_node() class MailGW: @@ -1362,6 +1427,7 @@ class MailGW: # in some rare cases, a particularly stuffed-up e-mail will make # its way into here... try to handle it gracefully + self.parsed_message = None sendto = message.getaddrlist('resent-from') if not sendto: sendto = message.getaddrlist('from') @@ -1451,77 +1517,8 @@ class MailGW: The following code expects an opened database and a try/finally that closes the database. ''' - parsed_message = self.parsed_message_class(self, message) - - # Filter out messages to ignore - parsed_message.handle_ignore() - - # Check for usage/help requests - parsed_message.handle_help() - - # Check if the subject line is valid - parsed_message.check_subject() - - # XXX Don't enable. This doesn't work yet. - # XXX once this works it should be moved to parsedMessage class -# "[^A-z.]tracker\+(?P[^\d\s]+)(?P\d+)\@some.dom.ain[^A-z.]" - # handle delivery to addresses like:tracker+issue25@some.dom.ain - # use the embedded issue number as our issue -# issue_re = config['MAILGW_ISSUE_ADDRESS_RE'] -# if issue_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 - - # Parse the subject line to get the importants parts - parsed_message.parse_subject() - - # check for registration OTK - if parsed_message.rego_confirm(): - return - - # get the classname - parsed_message.get_classname() - - # get the optional nodeid - parsed_message.get_nodeid() - - # Determine who the author is - parsed_message.get_author_id() - - # make sure they're allowed to edit or create this class - parsed_message.check_node_permissions() - - # author may have been created: - # commit author to database and re-open as author - parsed_message.commit_and_reopen_as_author() - - # Get the recipients list - parsed_message.get_recipients() - - # get the new/updated node props - parsed_message.get_props() - - # Handle PGP signed or encrypted messages - parsed_message.get_pgp_message() - - # extract content and attachments from message body - parsed_message.get_content_and_attachments() - - # put attachments into files linked to the issue - parsed_message.create_files() - - # create the message if there's a message body (content) - parsed_message.create_msg() - - # perform the node change / create - nodeid = parsed_message.create_node() + self.parsed_message = self.parsed_message_class(self, message) + nodeid = self.parsed_message.parse () # commit the changes to the DB self.db.commit() @@ -1658,7 +1655,17 @@ def uidFromAddress(db, address, create=1, **user_props): props = db.user.getprops() if props.has_key('alternate_addresses'): users = db.user.filter(None, {'alternate_addresses': address}) - user = extractUserFromList(db.user, users) + # We want an exact match of the email, not just a substring + # match. Otherwise e.g. support@example.com would match + # discuss-support@example.com which is not what we want. + found_users = [] + for u in users: + alt = db.user.get(u, 'alternate_addresses').split('\n') + for a in alt: + if a.strip().lower() == address.lower(): + found_users.append(u) + break + user = extractUserFromList(db.user, found_users) if user is not None: return user @@ -1688,7 +1695,7 @@ def uidFromAddress(db, address, create=1, **user_props): try: return db.user.create(username=trying, address=address, realname=realname, roles=db.config.NEW_EMAIL_USER_ROLES, - password=password.Password(password.generatePassword()), + password=password.Password(password.generatePassword(), config=db.config), **user_props) except exceptions.Reject: return 0