summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 7dfd71f)
raw | patch | inline | side by side (parent: 7dfd71f)
author | schlatterbeck <schlatterbeck@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 7 Oct 2011 14:21:57 +0000 (14:21 +0000) | ||
committer | schlatterbeck <schlatterbeck@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 7 Oct 2011 14:21:57 +0000 (14:21 +0000) |
now have a regression test. We now take care that bounce-messages for
incoming encrypted mails or mails where the policy dictates that
outgoing traffic should be encrypted is actually pgp-encrypted. Note
that the new pgp encrypt option for outgoing mails works only for
bounces for now.
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4654 57a73879-2fb5-44c3-a270-3262357dd7e2
incoming encrypted mails or mails where the policy dictates that
outgoing traffic should be encrypted is actually pgp-encrypted. Note
that the new pgp encrypt option for outgoing mails works only for
bounces for now.
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4654 57a73879-2fb5-44c3-a270-3262357dd7e2
diff --git a/CHANGES.txt b/CHANGES.txt
index 5a792dfc6423297bb1876edfcf3a4387f662c355..1cab75f26211ebf24d8fde373882d7992f02a72c 100644 (file)
--- a/CHANGES.txt
+++ b/CHANGES.txt
is addressed to support@example.com this would (wrongly) match. (Ralf)
- issue2550729: Fix password history display for anydbm backend, thanks
to Ralf Hemmecke for reporting. (Ralf)
is addressed to support@example.com this would (wrongly) match. (Ralf)
- issue2550729: Fix password history display for anydbm backend, thanks
to Ralf Hemmecke for reporting. (Ralf)
+- PGP support is again working (pyme API has changed significantly) and
+ we now have a regression test. We now take care that bounce-messages
+ for incoming encrypted mails or mails where the policy dictates that
+ outgoing traffic should be encrypted is actually pgp-encrypted. Note
+ that the new pgp encrypt option for outgoing mails works only for
+ bounces for now. (Ralf)
2011-07-15 1.4.19 (r4638)
2011-07-15 1.4.19 (r4638)
index ab43ca5f3f7a62f81392e2bf636b537ba716feda..6b9d05a7a2d8a0174c6df089b7b6c45629c49c67 100644 (file)
--- a/roundup/configuration.py
+++ b/roundup/configuration.py
), "Roundup Mail Gateway options"),
("pgp", (
(BooleanOption, "enable", "no",
), "Roundup Mail Gateway options"),
("pgp", (
(BooleanOption, "enable", "no",
- "Enable PGP processing. Requires pyme."),
+ "Enable PGP processing. Requires pyme. If you're planning\n"
+ "to send encrypted PGP mail to the tracker, you should also\n"
+ "enable the encrypt-option below, otherwise mail received\n"
+ "encrypted might be sent unencrypted to another user."),
(NullableOption, "roles", "",
"If specified, a comma-separated list of roles to perform\n"
"PGP processing on. If not specified, it happens for all\n"
(NullableOption, "roles", "",
"If specified, a comma-separated list of roles to perform\n"
"PGP processing on. If not specified, it happens for all\n"
- "users."),
+ "users. Note that received PGP messages (signed and/or\n"
+ "encrypted) will be processed with PGP even if the user\n"
+ "doesn't have one of the PGP roles, you can use this to make\n"
+ "PGP processing completely optional by defining a role here\n"
+ "and not assigning any users to that role."),
(NullableOption, "homedir", "",
"Location of PGP directory. Defaults to $HOME/.gnupg if\n"
"not specified."),
(NullableOption, "homedir", "",
"Location of PGP directory. Defaults to $HOME/.gnupg if\n"
"not specified."),
+ (BooleanOption, "encrypt", "no",
+ "Enable PGP encryption. All outgoing mails are encrypted.\n"
+ "This requires that keys for all users (with one of the gpg\n"
+ "roles above or all users if empty) are available. Note that\n"
+ "it makes sense to educate users to also send mails encrypted\n"
+ "to the tracker, to enforce this, set 'require_incoming'\n"
+ "option below (but see the note)."),
+ (Option, "require_incoming", "signed",
+ "Require that pgp messages received by roundup are either\n"
+ "'signed', 'encrypted' or 'both'. If encryption is required\n"
+ "we do not return the message (in clear) to the user but just\n"
+ "send an informational message that the message was rejected.\n"
+ "Note that this still presents known-plaintext to an attacker\n"
+ "when the users sends the mail a second time with encryption\n"
+ "turned on."),
), "OpenPGP mail processing options"),
("nosy", (
(RunDetectorOption, "messages_to_author", "no",
), "OpenPGP mail processing options"),
("nosy", (
(RunDetectorOption, "messages_to_author", "no",
diff --git a/roundup/mailer.py b/roundup/mailer.py
index a91baf2cee5ebfeaf705c09c6e8c6a3285d29f9a..b51c81d0ad755aa78f9e01c97e999407e451e831 100644 (file)
--- a/roundup/mailer.py
+++ b/roundup/mailer.py
from email.Utils import formatdate, formataddr, specialsre, escapesre
from email.Message import Message
from email.Header import Header
from email.Utils import formatdate, formataddr, specialsre, escapesre
from email.Message import Message
from email.Header import Header
+from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
+try:
+ import pyme, pyme.core
+except ImportError:
+ pyme = None
+
+
class MessageSendError(RuntimeError):
pass
class MessageSendError(RuntimeError):
pass
os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
time.tzset()
os.environ['TZ'] = get_timezone(self.config.TIMEZONE).tzname(None)
time.tzset()
- def get_standard_message(self, to, subject, author=None, multipart=False):
- '''Form a standard email message from Roundup.
-
+ def set_message_attributes(self, message, to, subject, author=None):
+ ''' Add attributes to a standard output message
"to" - recipients list
"subject" - Subject
"author" - (name, address) tuple or None for admin email
Subject and author are encoded using the EMAIL_CHARSET from the
config (default UTF-8).
"to" - recipients list
"subject" - Subject
"author" - (name, address) tuple or None for admin email
Subject and author are encoded using the EMAIL_CHARSET from the
config (default UTF-8).
-
- Returns a Message object.
'''
# encode header values if they need to be
charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
'''
# encode header values if they need to be
charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
else:
name = unicode(author[0], 'utf-8')
author = nice_sender_header(name, author[1], charset)
else:
name = unicode(author[0], 'utf-8')
author = nice_sender_header(name, author[1], charset)
-
- if multipart:
- message = MIMEMultipart()
- else:
- message = MIMEText("")
- message.set_charset(charset)
-
try:
message['Subject'] = subject.encode('ascii')
except UnicodeError:
try:
message['Subject'] = subject.encode('ascii')
except UnicodeError:
# finally, an aid to debugging problems
message['X-Roundup-Version'] = __version__
# finally, an aid to debugging problems
message['X-Roundup-Version'] = __version__
+ def get_standard_message(self, multipart=False):
+ '''Form a standard email message from Roundup.
+ Returns a Message object.
+ '''
+ charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
+ if multipart:
+ message = MIMEMultipart()
+ else:
+ message = MIMEText("")
+ message.set_charset(charset)
+
return message
def standard_message(self, to, subject, content, author=None):
return message
def standard_message(self, to, subject, content, author=None):
All strings are assumed to be UTF-8 encoded.
"""
All strings are assumed to be UTF-8 encoded.
"""
- message = self.get_standard_message(to, subject, author)
+ message = self.get_standard_message()
+ self.set_message_attributes(message, to, subject, author)
message.set_payload(content)
encode_quopri(message)
self.smtp_send(to, message.as_string())
def bounce_message(self, bounced_message, to, error,
message.set_payload(content)
encode_quopri(message)
self.smtp_send(to, message.as_string())
def bounce_message(self, bounced_message, to, error,
- subject='Failed issue tracker submission'):
+ subject='Failed issue tracker submission', crypt=False):
"""Bounce a message, attaching the failed submission.
Arguments:
"""Bounce a message, attaching the failed submission.
Arguments:
ERROR_MESSAGES_TO setting.
- error: the reason of failure as a string.
- subject: the subject as a string.
ERROR_MESSAGES_TO setting.
- error: the reason of failure as a string.
- subject: the subject as a string.
+ - crypt: require encryption with pgp for user -- applies only to
+ mail sent back to the user, not the dispatcher oder admin.
"""
"""
+ crypt_to = None
+ if crypt:
+ crypt_to = to
+ to = None
# see whether we should send to the dispatcher or not
dispatcher_email = getattr(self.config, "DISPATCHER_EMAIL",
getattr(self.config, "ADMIN_EMAIL"))
error_messages_to = getattr(self.config, "ERROR_MESSAGES_TO", "user")
if error_messages_to == "dispatcher":
to = [dispatcher_email]
# see whether we should send to the dispatcher or not
dispatcher_email = getattr(self.config, "DISPATCHER_EMAIL",
getattr(self.config, "ADMIN_EMAIL"))
error_messages_to = getattr(self.config, "ERROR_MESSAGES_TO", "user")
if error_messages_to == "dispatcher":
to = [dispatcher_email]
+ crypt = False
+ crypt_to = None
elif error_messages_to == "both":
elif error_messages_to == "both":
- to.append(dispatcher_email)
+ if crypt:
+ to = [dispatcher_email]
+ else:
+ to.append(dispatcher_email)
- message = self.get_standard_message(to, subject, multipart=True)
+ message = self.get_standard_message(multipart=True)
# add the error text
part = MIMEText('\n'.join(error))
# add the error text
part = MIMEText('\n'.join(error))
part = MIMEText(''.join(body))
message.attach(part)
part = MIMEText(''.join(body))
message.attach(part)
- # send
- try:
- self.smtp_send(to, message.as_string())
- except MessageSendError:
- # squash mail sending errors when bouncing mail
- # TODO this *could* be better, as we could notify admin of the
- # problem (even though the vast majority of bounce errors are
- # because of spam)
- pass
+ if to:
+ # send
+ self.set_message_attributes(message, to, subject)
+ try:
+ self.smtp_send(to, message.as_string())
+ except MessageSendError:
+ # squash mail sending errors when bouncing mail
+ # TODO this *could* be better, as we could notify admin of the
+ # problem (even though the vast majority of bounce errors are
+ # because of spam)
+ pass
+ if crypt_to:
+ plain = pyme.core.Data(message.as_string())
+ cipher = pyme.core.Data()
+ ctx = pyme.core.Context()
+ ctx.set_armor(1)
+ keys = []
+ adrs = []
+ for adr in crypt_to:
+ ctx.op_keylist_start(adr, 0)
+ # only first key per email
+ k = ctx.op_keylist_next()
+ if k is not None:
+ adrs.append(adr)
+ keys.append(k)
+ ctx.op_keylist_end()
+ crypt_to = adrs
+ if crypt_to:
+ try:
+ ctx.op_encrypt(keys, 1, plain, cipher)
+ cipher.seek(0,0)
+ message=MIMEMultipart('encrypted', boundary=None,
+ _subparts=None, protocol="application/pgp-encrypted")
+ part=MIMEBase('application', 'pgp-encrypted')
+ part.set_payload("Version: 1\r\n")
+ message.attach(part)
+ part=MIMEBase('application', 'octet-stream')
+ part.set_payload(cipher.read())
+ message.attach(part)
+ except pyme.GPGMEError:
+ crypt_to = None
+ if crypt_to:
+ self.set_message_attributes(message, crypt_to, subject)
+ try:
+ self.smtp_send(crypt_to, message.as_string())
+ except MessageSendError:
+ # ignore on error, see above.
+ pass
def exception_message(self):
'''Send a message to the admins with information about the latest
def exception_message(self):
'''Send a message to the admins with information about the latest
diff --git a/roundup/mailgw.py b/roundup/mailgw.py
index d269554f8783216a05f51de73ed062c15fa28ca3..1558c0f421718505d2d8258bdd3754a779d59aaf 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.""")
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):
diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py
index 19b3004b102d99a117c34da4ea07f78c1c398790..91d855c9282bfae8df9fdd5f59ffa092d57b303f 100644 (file)
--- a/roundup/roundupdb.py
+++ b/roundup/roundupdb.py
# create the message
mailer = Mailer(self.db.config)
# create the message
mailer = Mailer(self.db.config)
- message = mailer.get_standard_message(sendto, subject, author,
- multipart=message_files)
+ message = mailer.get_standard_message(multipart=message_files)
+ mailer.set_message_attributes(message, sendto, subject, author)
# set reply-to to the tracker
message['Reply-To'] = tracker_name
# set reply-to to the tracker
message['Reply-To'] = tracker_name
diff --git a/test/test_mailgw.py b/test/test_mailgw.py
index ea1a53a67c9906abaafba1c2bbf40dcfc4b4fafe..e0789ae2d015f6b2ab51ae3172605e22ea4ff89e 100644 (file)
--- a/test/test_mailgw.py
+++ b/test/test_mailgw.py
# TODO: test bcc
import unittest, tempfile, os, shutil, errno, imp, sys, difflib, rfc822, time
# TODO: test bcc
import unittest, tempfile, os, shutil, errno, imp, sys, difflib, rfc822, time
+from email.parser import FeedParser
try:
try:
handler.db = self.db
return handler
handler.db = self.db
return handler
- def _handle_mail(self, message, args=()):
+ def _handle_mail(self, message, args=(), trap_exc=0):
handler = self._create_mailgw(message, args)
handler = self._create_mailgw(message, args)
- handler.trapExceptions = 0
+ handler.trapExceptions = trap_exc
return handler.main(StringIO(message))
def _get_mail(self):
return handler.main(StringIO(message))
def _get_mail(self):
This is a test submission of a new issue.
'''
This is a test submission of a new issue.
'''
- handler = self._create_mailgw(message)
- # we want a bounce message:
- handler.trapExceptions = 1
- ret = handler.main(StringIO(message))
+ # trap_exc=1: we want a bounce message:
+ ret = self._handle_mail(message, trap_exc=1)
self.compareMessages(self._get_mail(),
'''FROM: Roundup issue tracker <roundup-admin@your.tracker.email.domain.example>
TO: nonexisting@bork.bork.bork
self.compareMessages(self._get_mail(),
'''FROM: Roundup issue tracker <roundup-admin@your.tracker.email.domain.example>
TO: nonexisting@bork.bork.bork
pgphome = 'pgp-test-home'
def setUp(self):
MailgwTestAbstractBase.setUp(self)
pgphome = 'pgp-test-home'
def setUp(self):
MailgwTestAbstractBase.setUp(self)
- self.db.security.addRole (name = 'pgp', description = 'PGP Role')
+ self.db.security.addRole(name = 'pgp', description = 'PGP Role')
self.instance.config['PGP_HOMEDIR'] = self.pgphome
self.instance.config['PGP_ROLES'] = 'pgp'
self.instance.config['PGP_ENABLE'] = True
self.instance.config['PGP_HOMEDIR'] = self.pgphome
self.instance.config['PGP_ROLES'] = 'pgp'
self.instance.config['PGP_ENABLE'] = True
+ self.instance.config['MAIL_DOMAIN'] = 'example.com'
+ self.instance.config['ADMIN_EMAIL'] = 'roundup-admin@example.com'
self.db.user.set(self.john_id, roles='User,pgp')
os.mkdir(self.pgphome)
os.environ['GNUPGHOME'] = self.pgphome
self.db.user.set(self.john_id, roles='User,pgp')
os.mkdir(self.pgphome)
os.environ['GNUPGHOME'] = self.pgphome
if os.path.exists(self.pgphome):
shutil.rmtree(self.pgphome)
if os.path.exists(self.pgphome):
shutil.rmtree(self.pgphome)
- def testUnsignedMessage(self):
+ def testPGPUnsignedMessage(self):
self.assertRaises(MailUsageError, self._handle_mail,
'''Content-Type: text/plain;
charset="iso-8859-1"
self.assertRaises(MailUsageError, self._handle_mail,
'''Content-Type: text/plain;
charset="iso-8859-1"
This is no pgp signed message.
''')
This is no pgp signed message.
''')
- def testSignedMessage(self):
- nodeid = self._handle_mail('''Content-Disposition: inline
+ signed_msg = '''Content-Disposition: inline
From: John Doe <john@test.test>
To: issue_tracker@your.tracker.email.domain.example
Subject: [issue] Testing signed message...
From: John Doe <john@test.test>
To: issue_tracker@your.tracker.email.domain.example
Subject: [issue] Testing signed message...
-----END PGP SIGNATURE-----
--cWoXeonUoKmBZSoM--
-----END PGP SIGNATURE-----
--cWoXeonUoKmBZSoM--
-''')
- m = self.db.issue.get (nodeid, 'messages') [0]
+'''
+
+ def testPGPSignedMessage(self):
+ nodeid = self._handle_mail(self.signed_msg)
+ m = self.db.issue.get(nodeid, 'messages')[0]
self.assertEqual(self.db.msg.get(m, 'content'),
'This is a pgp signed message.')
self.assertEqual(self.db.msg.get(m, 'content'),
'This is a pgp signed message.')
+ def testPGPSignedMessageFail(self):
+ # require both, signing and encryption
+ self.instance.config['PGP_REQUIRE_INCOMING'] = 'both'
+ self.assertRaises(MailUsageError, self._handle_mail, self.signed_msg)
+
+ encrypted_msg = '''Content-Disposition: inline
+From: John Doe <john@test.test>
+To: roundup-admin@example.com
+Subject: [issue] Testing encrypted message...
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="d6Gm4EdcadzBjdND"
+
+--d6Gm4EdcadzBjdND
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--d6Gm4EdcadzBjdND
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+hQEMAzfeQttq+Q2YAQf9FxCtZVgC7jAy6UkeAJ1imCpnh9DgKA5w40OFtrY4mVAp
+cL7kCkvGvJCW7uQZrmSgIiYaZGLI3GS42XutORC6E6PzBEW0fJUMIXYmoSd0OFeY
+3H2+854qu37W/uCOWM9OnPFIH8g8q8DgYy88i0goM+Ot9Q96yFfJ7QymanOZJgVa
+MNC+oKDiIZKiE3PCwtGr+8CHZN/9J6O4FeJijBlr09C5LXc+Nif5T0R0nt17MAns
+9g2UvGxW8U24NAS1mOg868U05hquLPIcFz9jGZGknJu7HBpOkQ9GjKqkzN8pgZVN
+VbN8IdDqi0QtRKE44jtWQlyNlESMjv6GtC2V9F6qKNK8AfHtBexDhyv4G9cPFFNO
+afQ6e4dPi89RYIQyydtwiqao8fj6jlAy2Z1cbr7YxwBG7BeUZv9yis7ShaAIo78S
+82MrCYpSjfHNwKiSfC5yITw22Uv4wWgixVdAsaSdtBqEKXJPG9LNey18ArsBjSM1
+P81iDOWUp/uyIe5ZfvNI38BBxEYslPTUlDk2GB8J2Vun7IWHoj9a4tY3IotC9jBr
+5Qnigzqrt7cJZX6OrN0c+wnOjXbMGYXmgSs4jeM=
+=XX5Q
+-----END PGP MESSAGE-----
+
+--d6Gm4EdcadzBjdND--
+'''
+ def testPGPEncryptedUnsignedMessageError(self):
+ self.assertRaises(MailUsageError, self._handle_mail, self.encrypted_msg)
+
+ def testPGPEncryptedUnsignedMessage(self):
+ # no error if we don't require a signature:
+ self.instance.config['PGP_REQUIRE_INCOMING'] = 'encrypted'
+ nodeid = self._handle_mail (self.encrypted_msg)
+ m = self.db.issue.get(nodeid, 'messages')[0]
+ self.assertEqual(self.db.msg.get(m, 'content'),
+ 'This is the text to be encrypted')
+
+ def testPGPEncryptedUnsignedMessageFromNonPGPUser(self):
+ msg = self.encrypted_msg.replace('John Doe <john@test.test>',
+ '"Contrary, Mary" <mary@test.test>')
+ nodeid = self._handle_mail (msg)
+ m = self.db.issue.get(nodeid, 'messages')[0]
+ self.assertEqual(self.db.msg.get(m, 'content'),
+ 'This is the text to be encrypted')
+ self.assertEqual(self.db.msg.get(m, 'author'), self.mary_id)
+
+ # check that a bounce-message that is triggered *after*
+ # decrypting is properly encrypted:
+ def testPGPEncryptedUnsignedMessageCheckBounce(self):
+ # allow non-signed msg
+ self.instance.config['PGP_REQUIRE_INCOMING'] = 'encrypted'
+ # don't allow creation of message, trigger error *after* decrypt
+ self.db.user.set(self.john_id, roles='pgp')
+ self.db.security.addPermissionToRole('pgp', 'Email Access')
+ self.db.security.addPermissionToRole('pgp', 'Create', 'issue')
+ # trap_exc=1: we want a bounce message:
+ self._handle_mail(self.encrypted_msg, trap_exc=1)
+ m = self._get_mail()
+ fp = FeedParser()
+ fp.feed(m)
+ parts = fp.close().get_payload()
+ self.assertEqual(len(parts),2)
+ self.assertEqual(parts[0].get_payload().strip(), 'Version: 1')
+ crypt = pyme.core.Data(parts[1].get_payload())
+ plain = pyme.core.Data()
+ ctx = pyme.core.Context()
+ res = ctx.op_decrypt(crypt, plain)
+ self.assertEqual(res, None)
+ plain.seek(0,0)
+ fp = FeedParser()
+ fp.feed(plain.read())
+ parts = fp.close().get_payload()
+ self.assertEqual(len(parts),2)
+ self.assertEqual(parts[0].get_payload().strip(),
+ 'You are not permitted to create messages.')
+ self.assertEqual(parts[1].get_payload().strip(),
+ '''Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+This is the text to be encrypted''')
+
+
+ def testPGPEncryptedSignedMessage(self):
+ # require both, signing and encryption
+ self.instance.config['PGP_REQUIRE_INCOMING'] = 'both'
+ nodeid = self._handle_mail('''Content-Disposition: inline
+From: John Doe <john@test.test>
+To: roundup-admin@example.com
+Subject: Testing encrypted and signed message
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="ReaqsoxgOBHFXBhH"
+
+--ReaqsoxgOBHFXBhH
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--ReaqsoxgOBHFXBhH
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+hQEMAzfeQttq+Q2YAQf+NaC3r8qBURQqxHH9IAP4vg0QAP2yj3n0v6guo1lRf5BA
+EUfTQ3jc3chxLvzTgoUIuMOvhlNroqR1lgLwhfSTCyuKWDZa+aVNiSgsB2MD44Xd
+mAkKKmnmOGLmfbICbPQZxl4xNhCMTHiAy1xQE6mTj/+pEAq5XxjJUwn/gJ3O1Wmd
+NyWtJY2N+TRbxUVB2WhG1j9J1D2sjhG26TciE8JeuLDZzaiVNOW9YlX2Lw5KtlkR
+Hkgw6Xme06G0XXZUcm9JuBU/7oFP/tSrC1tBsnVlq1pZYf6AygIBdXWb9gD/WmXh
+7Eu/xCKrw4RFnXnTgmBz/NHRfVDkfdSscZqexnG1D9LAwQHSuVf8sxDPNesv0W+8
+e49loVjvU+Y0BCFQAbWSW4iOEUYZpW/ITRE4+wIqMXZbAraeBV0KPZ4hAa3qSmf+
+oZBRcbzssL163Odx/OHRuK2J2CHC654+crrlTBnxd/RUKgRbSUKwrZzB2G6OPcGv
+wfiqXsY+XvSZtTbWuvUJxePh8vhhhjpuo1JtlrYc3hZ9OYgoCoV1JiLl5c60U5Es
+oUT9GDl1Qsgb4dF4TJ1IBj+riYiocYpJxPhxzsy6liSLNy2OA6VEjG0FGk53+Ok9
+7UzOA+WaHJHSXafZzrdP1TWJUFlOMA+dOgTKpH69eL1+IRfywOjEwp1UNSbLnJpc
+D0QQLwIFttplKvYkn0DZByJCVnIlGkl4s5LM5rnc8iecX8Jad0iRIlPV6CVM+Nso
+WdARUfyJfXAmz8uk4f2sVfeMu1gdMySdjvxwlgHDJdBPIG51r2b8L/NCTiC57YjF
+zGhS06FLl3V1xx6gBlpqQHjut3efrAGpXGBVpnTJMOcgYAk=
+=jt/n
+-----END PGP MESSAGE-----
+
+--ReaqsoxgOBHFXBhH--
+''')
+ m = self.db.issue.get(nodeid, 'messages')[0]
+ self.assertEqual(self.db.msg.get(m, 'content'),
+ 'This is the text of a signed and encrypted email.')
+
+
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MailgwTestCase))
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MailgwTestCase))