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)
   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)
@@ -799,14 +799,36 @@ SETTINGS = (
     ), "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",
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.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
 
@@ -64,17 +71,14 @@ class Mailer:
             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')
@@ -85,13 +89,6 @@ class Mailer:
         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:
@@ -114,6 +111,17 @@ class Mailer:
         # 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):
@@ -127,13 +135,14 @@ class Mailer:
 
         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:
@@ -143,18 +152,29 @@ class Mailer:
           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))
@@ -175,15 +195,54 @@ class Mailer:
         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
index d269554f8783216a05f51de73ed062c15fa28ca3..1558c0f421718505d2d8258bdd3754a779d59aaf 100644 (file)
@@ -159,10 +159,12 @@ def gpgh_key_getall(key, attr):
     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)
@@ -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
                         _("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
@@ -452,16 +457,18 @@ class Message(mimetools.Message):
         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.")
 
@@ -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()
         # 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
@@ -550,6 +558,7 @@ class parsedMessage:
         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:
@@ -991,22 +1000,33 @@ Subject was: "%(subject)s"
             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.""")
@@ -1449,6 +1469,12 @@ class MailGW:
             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:
@@ -1466,12 +1492,18 @@ class MailGW:
             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
@@ -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')
             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())
@@ -1523,6 +1558,7 @@ class MailGW:
         # 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):
index 19b3004b102d99a117c34da4ea07f78c1c398790..91d855c9282bfae8df9fdd5f59ffa092d57b303f 100644 (file)
@@ -429,8 +429,8 @@ class IssueClass:
             # 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
index ea1a53a67c9906abaafba1c2bbf40dcfc4b4fafe..e0789ae2d015f6b2ab51ae3172605e22ea4ff89e 100644 (file)
@@ -14,6 +14,7 @@
 # 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:
@@ -164,9 +165,9 @@ class MailgwTestAbstractBase(unittest.TestCase, DiffHelper):
         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):
@@ -1875,10 +1876,8 @@ Subject: [issue] Testing nonexisting user...
 
 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
@@ -3067,10 +3066,12 @@ class MailgwPGPTestCase(MailgwTestAbstractBase):
     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
@@ -3090,7 +3091,7 @@ class MailgwPGPTestCase(MailgwTestAbstractBase):
         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"
@@ -3102,8 +3103,7 @@ Subject: [issue] Testing non-signed message...
 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...
@@ -3133,11 +3133,158 @@ ZQ4K6R3m3AOw7BLdvZs=
 -----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))