From 6ba44a7c3806ba5f40562a0e390605a25145a01b Mon Sep 17 00:00:00 2001 From: schlatterbeck Date: Fri, 7 Oct 2011 14:21:57 +0000 Subject: [PATCH] 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. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4654 57a73879-2fb5-44c3-a270-3262357dd7e2 --- CHANGES.txt | 6 ++ roundup/configuration.py | 26 +++++- roundup/mailer.py | 109 +++++++++++++++++++------ roundup/mailgw.py | 72 ++++++++++++----- roundup/roundupdb.py | 4 +- test/test_mailgw.py | 171 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 329 insertions(+), 59 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5a792df..1cab75f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/roundup/configuration.py b/roundup/configuration.py index ab43ca5..6b9d05a 100644 --- a/roundup/configuration.py +++ b/roundup/configuration.py @@ -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", diff --git a/roundup/mailer.py b/roundup/mailer.py index a91baf2..b51c81d 100644 --- a/roundup/mailer.py +++ b/roundup/mailer.py @@ -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 diff --git a/roundup/mailgw.py b/roundup/mailgw.py index d269554..1558c0f 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -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): diff --git a/roundup/roundupdb.py b/roundup/roundupdb.py index 19b3004..91d855c 100644 --- a/roundup/roundupdb.py +++ b/roundup/roundupdb.py @@ -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 diff --git a/test/test_mailgw.py b/test/test_mailgw.py index ea1a53a..e0789ae 100644 --- a/test/test_mailgw.py +++ b/test/test_mailgw.py @@ -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 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 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 +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 ', + '"Contrary, Mary" ') + 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 +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)) -- 2.30.2