Code

PGP support is again working (pyme API has changed significantly) and we
authorschlatterbeck <schlatterbeck@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 7 Oct 2011 14:21:57 +0000 (14:21 +0000)
committerschlatterbeck <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

CHANGES.txt
roundup/configuration.py
roundup/mailer.py
roundup/mailgw.py
roundup/roundupdb.py
test/test_mailgw.py

index 5a792dfc6423297bb1876edfcf3a4387f662c355..1cab75f26211ebf24d8fde373882d7992f02a72c 100644 (file)
@@ -29,6 +29,12 @@ Fixed:
   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)
 
index ab43ca5f3f7a62f81392e2bf636b537ba716feda..6b9d05a7a2d8a0174c6df089b7b6c45629c49c67 100644 (file)
@@ -799,14 +799,36 @@ SETTINGS = (
     ), "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"
-            "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."),
+        (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",
index a91baf2cee5ebfeaf705c09c6e8c6a3285d29f9a..b51c81d0ad755aa78f9e01c97e999407e451e831 100644 (file)
@@ -12,9 +12,16 @@ from roundup.date import get_timezone, Date
 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
 
+try:
+    import pyme, pyme.core
+except ImportError:
+    pyme = None
+
+
 class MessageSendError(RuntimeError):
     pass
 
@@ -64,17 +71,14 @@ class Mailer:
             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).
-
-        Returns a Message object.
         '''
         # encode header values if they need to be
         charset = getattr(self.config, 'EMAIL_CHARSET', 'utf-8')
@@ -85,13 +89,6 @@ class Mailer:
         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:
@@ -114,6 +111,17 @@ class Mailer:
         # 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):
@@ -127,13 +135,14 @@ class Mailer:
 
         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,
-                       subject='Failed issue tracker submission'):
+                       subject='Failed issue tracker submission', crypt=False):
         """Bounce a message, attaching the failed submission.
 
         Arguments:
@@ -143,18 +152,29 @@ class Mailer:
           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]
+            crypt = False
+            crypt_to = None
         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))
@@ -175,15 +195,54 @@ class Mailer:
         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
index d269554f8783216a05f51de73ed062c15fa28ca3..1558c0f421718505d2d8258bdd3754a779d59aaf 100644 (file)
@@ -159,10 +159,12 @@ def gpgh_key_getall(key, 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
-        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)
@@ -188,7 +190,10 @@ def check_pgp_sigs(sigs, gpgctx, author):
                         _("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
@@ -452,16 +457,18 @@ class Message(mimetools.Message):
         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
-            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.
-        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.")
 
@@ -478,7 +485,8 @@ class Message(mimetools.Message):
         # 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
@@ -550,6 +558,7 @@ class parsedMessage:
         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:
@@ -991,22 +1000,33 @@ Subject was: "%(subject)s"
             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
+            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():
-                # replace message with the contents of the decrypted
+                # Replace message with the contents of the decrypted
                 # 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.""")
@@ -1449,6 +1469,12 @@ class MailGW:
             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:
@@ -1466,12 +1492,18 @@ class MailGW:
             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))
-            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
@@ -1492,7 +1524,10 @@ class MailGW:
             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())
@@ -1523,6 +1558,7 @@ class MailGW:
         # commit the changes to the DB
         self.db.commit()
 
+        self.parsed_message = None
         return nodeid
 
     def get_class_arguments(self, class_type, classname=None):
index 19b3004b102d99a117c34da4ea07f78c1c398790..91d855c9282bfae8df9fdd5f59ffa092d57b303f 100644 (file)
@@ -429,8 +429,8 @@ class IssueClass:
             # 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
index ea1a53a67c9906abaafba1c2bbf40dcfc4b4fafe..e0789ae2d015f6b2ab51ae3172605e22ea4ff89e 100644 (file)
@@ -14,6 +14,7 @@
 # TODO: test bcc
 
 import unittest, tempfile, os, shutil, errno, imp, sys, difflib, rfc822, time
+from email.parser import FeedParser
 
 
 try:
@@ -164,9 +165,9 @@ class MailgwTestAbstractBase(unittest.TestCase, DiffHelper):
         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.trapExceptions = 0
+        handler.trapExceptions = trap_exc
         return handler.main(StringIO(message))
 
     def _get_mail(self):
@@ -1875,10 +1876,8 @@ Subject: [issue] Testing nonexisting user...
 
 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
@@ -3067,10 +3066,12 @@ class MailgwPGPTestCase(MailgwTestAbstractBase):
     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['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
@@ -3090,7 +3091,7 @@ class MailgwPGPTestCase(MailgwTestAbstractBase):
         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"
@@ -3102,8 +3103,7 @@ Subject: [issue] Testing non-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...
@@ -3133,11 +3133,158 @@ ZQ4K6R3m3AOw7BLdvZs=
 -----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.')
 
+    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))