diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index d269554f8783216a05f51de73ed062c15fa28ca3..7818d21b8652e940080963638ccd113d20d750c9 100644 (file)
--- a/roundup/mailgw.py
+++ b/roundup/mailgw.py
for u in key.uids:
yield getattr(u, attr)
for u in key.uids:
yield getattr(u, attr)
-def check_pgp_sigs(sigs, gpgctx, author):
+def check_pgp_sigs(sigs, gpgctx, author, may_be_unsigned=False):
''' Theoretically a PGP message can have several signatures. GPGME
returns status on all signatures in a list. Walk that list
''' Theoretically a PGP message can have several signatures. GPGME
returns status on all signatures in a list. Walk that list
- looking for the author's signature
+ looking for the author's signature. Note that even if incoming
+ signatures are not required, the processing fails if there is an
+ invalid signature.
'''
for sig in sigs:
key = gpgctx.get_key(sig.fpr, False)
'''
for sig in sigs:
key = gpgctx.get_key(sig.fpr, False)
_("Invalid PGP signature detected.")
# we couldn't find a key belonging to the author of the email
_("Invalid PGP signature detected.")
# we couldn't find a key belonging to the author of the email
- raise MailUsageError, _("Message signed with unknown key: %s") % sig.fpr
+ if sigs:
+ raise MailUsageError, _("Message signed with unknown key: %s") % sig.fpr
+ elif not may_be_unsigned:
+ raise MailUsageError, _("Unsigned Message")
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
class Message(mimetools.Message):
''' subclass mimetools.Message so we can retrieve the parts of the
return self.gettype() == 'multipart/encrypted' \
and self.typeheader.find('protocol="application/pgp-encrypted"') != -1
return self.gettype() == 'multipart/encrypted' \
and self.typeheader.find('protocol="application/pgp-encrypted"') != -1
- def decrypt(self, author):
+ def decrypt(self, author, may_be_unsigned=False):
''' decrypt an OpenPGP MIME message
''' decrypt an OpenPGP MIME message
- This message must be signed as well as encrypted using the "combined"
- method. The decrypted contents are returned as a new message.
+ This message must be signed as well as encrypted using the
+ "combined" method if incoming signatures are configured.
+ The decrypted contents are returned as a new message.
'''
(hdr, msg) = self.getparts()
# According to the RFC 3156 encrypted mail must have exactly two parts.
# The first part contains the control information. Let's verify that
# the message meets the RFC before we try to decrypt it.
'''
(hdr, msg) = self.getparts()
# According to the RFC 3156 encrypted mail must have exactly two parts.
# The first part contains the control information. Let's verify that
# the message meets the RFC before we try to decrypt it.
- if hdr.getbody() != 'Version: 1' or hdr.gettype() != 'application/pgp-encrypted':
+ if hdr.getbody().strip() != 'Version: 1' \
+ or hdr.gettype() != 'application/pgp-encrypted':
raise MailUsageError, \
_("Unknown multipart/encrypted version.")
raise MailUsageError, \
_("Unknown multipart/encrypted version.")
# key to send it to us. now check the signatures to see if it
# was signed by someone we trust
result = context.op_verify_result()
# key to send it to us. now check the signatures to see if it
# was signed by someone we trust
result = context.op_verify_result()
- check_pgp_sigs(result.signatures, context, author)
+ check_pgp_sigs(result.signatures, context, author,
+ may_be_unsigned = may_be_unsigned)
plaintext.seek(0,0)
# pyme.core.Data implements a seek method with a different signature
plaintext.seek(0,0)
# pyme.core.Data implements a seek method with a different signature
self.props = None
self.content = None
self.attachments = None
self.props = None
self.content = None
self.attachments = None
+ self.crypt = False
def handle_ignore(self):
''' Check to see if message can be safely ignored:
def handle_ignore(self):
''' Check to see if message can be safely ignored:
else:
return True
else:
return True
- if self.config.PGP_ENABLE and pgp_role():
+ if self.config.PGP_ENABLE:
+ if pgp_role() and self.config.PGP_ENCRYPT:
+ self.crypt = True
assert pyme, 'pyme is not installed'
# signed/encrypted mail must come from the primary address
author_address = self.db.user.get(self.author, 'address')
if self.config.PGP_HOMEDIR:
os.environ['GNUPGHOME'] = self.config.PGP_HOMEDIR
assert pyme, 'pyme is not installed'
# signed/encrypted mail must come from the primary address
author_address = self.db.user.get(self.author, 'address')
if self.config.PGP_HOMEDIR:
os.environ['GNUPGHOME'] = self.config.PGP_HOMEDIR
+ if self.config.PGP_REQUIRE_INCOMING in ('encrypted', 'both') \
+ and pgp_role() and not self.message.pgp_encrypted():
+ raise MailUsageError, _(
+ "This tracker has been configured to require all email "
+ "be PGP encrypted.")
if self.message.pgp_signed():
self.message.verify_signature(author_address)
elif self.message.pgp_encrypted():
if self.message.pgp_signed():
self.message.verify_signature(author_address)
elif self.message.pgp_encrypted():
- # replace message with the contents of the decrypted
+ # Replace message with the contents of the decrypted
# message for content extraction
# message for content extraction
- # TODO: encrypted message handling is far from perfect
- # bounces probably include the decrypted message, for
- # instance :(
- self.message = self.message.decrypt(author_address)
- else:
+ # Note: the bounce-handling code now makes sure that
+ # either the encrypted mail received is sent back or
+ # that the error message is encrypted if needed.
+ encr_only = self.config.PGP_REQUIRE_INCOMING == 'encrypted'
+ encr_only = encr_only or not pgp_role()
+ self.crypt = True
+ self.message = self.message.decrypt(author_address,
+ may_be_unsigned = encr_only)
+ elif pgp_role():
raise MailUsageError, _("""
This tracker has been configured to require all email be PGP signed or
encrypted.""")
raise MailUsageError, _("""
This tracker has been configured to require all email be PGP signed or
encrypted.""")
# method returns something that evaluates to True.
method_list = [
# Filter out messages to ignore
# method returns something that evaluates to True.
method_list = [
# Filter out messages to ignore
- (handle_ignore, False),
+ ("handle_ignore", False),
# Check for usage/help requests
# Check for usage/help requests
- (handle_help, False),
+ ("handle_help", False),
# Check if the subject line is valid
# Check if the subject line is valid
- (check_subject, False),
+ ("check_subject", False),
# get importants parts from subject
# get importants parts from subject
- (parse_subject, False),
+ ("parse_subject", False),
# check for registration OTK
# check for registration OTK
- (rego_confirm, True),
+ ("rego_confirm", True),
# get the classname
# get the classname
- (get_classname, False),
+ ("get_classname", False),
# get the optional nodeid:
# get the optional nodeid:
- (get_nodeid, False),
+ ("get_nodeid", False),
# Determine who the author is:
# Determine who the author is:
- (get_author_id, False),
+ ("get_author_id", False),
# allowed to edit or create this class?
# allowed to edit or create this class?
- (check_permissions, False),
+ ("check_permissions", False),
# author may have been created:
# commit author to database and re-open as author
# author may have been created:
# commit author to database and re-open as author
- (commit_and_reopen_as_author, False),
+ ("commit_and_reopen_as_author", False),
# Get the recipients list
# Get the recipients list
- (get_recipients, False),
+ ("get_recipients", False),
# get the new/updated node props
# get the new/updated node props
- (get_props, False),
+ ("get_props", False),
# Handle PGP signed or encrypted messages
# Handle PGP signed or encrypted messages
- (get_pgp_message, False),
+ ("get_pgp_message", False),
# extract content and attachments from message body:
# extract content and attachments from message body:
- (get_content_and_attachments, False),
+ ("get_content_and_attachments", False),
# put attachments into files linked to the issue:
# put attachments into files linked to the issue:
- (create_files, False),
+ ("create_files", False),
# create the message if there's a message body (content):
# create the message if there's a message body (content):
- (create_msg, False),
+ ("create_msg", False),
]
def parse (self):
]
def parse (self):
- for method, flag in self.method_list:
- ret = method(self)
+ for methodname, flag in self.method_list:
+ method = getattr (self, methodname)
+ ret = method()
if flag and ret:
return
# perform the node change / create:
if flag and ret:
return
# perform the node change / create:
return self.handle_message(message)
# no, we want to trap exceptions
return self.handle_message(message)
# no, we want to trap exceptions
+ # Note: by default we return the message received not the
+ # internal state of the parsedMessage -- except for
+ # MailUsageError, Unauthorized and for unknown exceptions. For
+ # the latter cases we make sure the error message is encrypted
+ # if needed (if it either was received encrypted or pgp
+ # processing is turned on for the user).
try:
return self.handle_message(message)
except MailUsageHelp:
try:
return self.handle_message(message)
except MailUsageHelp:
m.append(str(value))
m.append('\n\nMail Gateway Help\n=================')
m.append(fulldoc)
m.append(str(value))
m.append('\n\nMail Gateway Help\n=================')
m.append(fulldoc)
- self.mailer.bounce_message(message, [sendto[0][1]], m)
+ if self.parsed_message:
+ message = self.parsed_message.message
+ crypt = self.parsed_message.crypt
+ self.mailer.bounce_message(message, [sendto[0][1]], m, crypt=crypt)
except Unauthorized, value:
# just inform the user that he is not authorized
m = ['']
m.append(str(value))
except Unauthorized, value:
# just inform the user that he is not authorized
m = ['']
m.append(str(value))
- self.mailer.bounce_message(message, [sendto[0][1]], m)
+ if self.parsed_message:
+ message = self.parsed_message.message
+ crypt = self.parsed_message.crypt
+ self.mailer.bounce_message(message, [sendto[0][1]], m, crypt=crypt)
except IgnoreMessage:
# do not take any action
# this exception is thrown when email should be ignored
except IgnoreMessage:
# do not take any action
# this exception is thrown when email should be ignored
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('An unexpected error occurred during the processing')
m.append('of your message. The tracker administrator is being')
m.append('notified.\n')
- self.mailer.bounce_message(message, [sendto[0][1]], m)
+ if self.parsed_message:
+ message = self.parsed_message.message
+ crypt = self.parsed_message.crypt
+ self.mailer.bounce_message(message, [sendto[0][1]], m, crypt=crypt)
m.append('----------------')
m.append(traceback.format_exc())
m.append('----------------')
m.append(traceback.format_exc())
# commit the changes to the DB
self.db.commit()
# commit the changes to the DB
self.db.commit()
+ self.parsed_message = None
return nodeid
def get_class_arguments(self, class_type, classname=None):
return nodeid
def get_class_arguments(self, class_type, classname=None):