Add gpgme-tool.socket-path configuration to smtplib.conf.
[pgp-mime.git] / pgp_mime / pgp.py
1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pgp-mime.
4 #
5 # pgp-mime is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pgp-mime is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pgp-mime.  If not, see <http://www.gnu.org/licenses/>.
16
17 import copy as _copy
18 from email import message_from_bytes as _message_from_bytes
19 from email.encoders import encode_7or8bit as _encode_7or8bit
20 from email.mime.application import MIMEApplication as _MIMEApplication
21 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
22
23 from . import LOG as _LOG
24 from .crypt import sign_and_encrypt_bytes as _sign_and_encrypt_bytes
25 from .crypt import verify_bytes as _verify_bytes
26 from .email import email_targets as _email_targets
27 from .email import strip_bcc as _strip_bcc
28
29
30 def sign(message, signers=None, **kwargs):
31     r"""Sign a ``Message``, returning the signed version.
32
33     multipart/signed
34     +-> text/plain                 (body)
35     +-> application/pgp-signature  (signature)
36
37     >>> from pgp_mime.email import encodedMIMEText
38     >>> message = encodedMIMEText('Hi\nBye')
39     >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
40     >>> signed.set_boundary('boundsep')
41     >>> print(signed.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
42     Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
43     MIME-Version: 1.0
44     Content-Disposition: inline
45     <BLANKLINE>
46     --boundsep
47     Content-Type: text/plain; charset="us-ascii"
48     MIME-Version: 1.0
49     Content-Transfer-Encoding: 7bit
50     Content-Disposition: inline
51     <BLANKLINE>
52     Hi
53     Bye
54     --boundsep
55     MIME-Version: 1.0
56     Content-Transfer-Encoding: 7bit
57     Content-Description: OpenPGP digital signature
58     Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
59     <BLANKLINE>
60     -----BEGIN PGP SIGNATURE-----
61     Version: GnuPG...
62     -----END PGP SIGNATURE-----
63     <BLANKLINE>
64     --boundsep--
65
66     >>> from email.mime.multipart import MIMEMultipart
67     >>> message = MIMEMultipart()
68     >>> message.attach(encodedMIMEText('Part A'))
69     >>> message.attach(encodedMIMEText('Part B'))
70     >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
71     >>> signed.set_boundary('boundsep')
72     >>> print(signed.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
73     Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
74     MIME-Version: 1.0
75     Content-Disposition: inline
76     <BLANKLINE>
77     --boundsep
78     Content-Type: multipart/mixed; boundary="===============...=="
79     MIME-Version: 1.0
80     <BLANKLINE>
81     --===============...==
82     Content-Type: text/plain; charset="us-ascii"
83     MIME-Version: 1.0
84     Content-Transfer-Encoding: 7bit
85     Content-Disposition: inline
86     <BLANKLINE>
87     Part A
88     --===============...==
89     Content-Type: text/plain; charset="us-ascii"
90     MIME-Version: 1.0
91     Content-Transfer-Encoding: 7bit
92     Content-Disposition: inline
93     <BLANKLINE>
94     Part B
95     --===============...==--
96     --boundsep
97     MIME-Version: 1.0
98     Content-Transfer-Encoding: 7bit
99     Content-Description: OpenPGP digital signature
100     Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
101     <BLANKLINE>
102     -----BEGIN PGP SIGNATURE-----
103     Version: GnuPG...
104     -----END PGP SIGNATURE-----
105     <BLANKLINE>
106     --boundsep--
107     """
108     body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
109     # use email.policy.SMTP once we get Python 3.3
110     signature = str(_sign_and_encrypt_bytes(data=body, **kwargs), 'us-ascii')
111     sig = _MIMEApplication(
112         _data=signature,
113         _subtype='pgp-signature; name="signature.asc"',
114         _encoder=_encode_7or8bit)
115     sig['Content-Description'] = 'OpenPGP digital signature'
116     sig.set_charset('us-ascii')
117
118     msg = _MIMEMultipart(
119         'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
120     msg.attach(message)
121     msg.attach(sig)
122     msg['Content-Disposition'] = 'inline'
123     return msg
124
125 def encrypt(message, recipients=None, **kwargs):
126     r"""Encrypt a ``Message``, returning the encrypted version.
127
128     multipart/encrypted
129     +-> application/pgp-encrypted  (control information)
130     +-> application/octet-stream   (body)
131
132     >>> from pgp_mime.email import encodedMIMEText
133     >>> message = encodedMIMEText('Hi\nBye')
134     >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
135     >>> encrypted = encrypt(message)
136     >>> encrypted.set_boundary('boundsep')
137     >>> print(encrypted.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
138     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
139     MIME-Version: 1.0
140     Content-Disposition: inline
141     <BLANKLINE>
142     --boundsep
143     MIME-Version: 1.0
144     Content-Transfer-Encoding: 7bit
145     Content-Type: application/pgp-encrypted; charset="us-ascii"
146     <BLANKLINE>
147     Version: 1
148     <BLANKLINE>
149     --boundsep
150     MIME-Version: 1.0
151     Content-Transfer-Encoding: 7bit
152     Content-Description: OpenPGP encrypted message
153     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
154     <BLANKLINE>
155     -----BEGIN PGP MESSAGE-----
156     Version: GnuPG...
157     -----END PGP MESSAGE-----
158     <BLANKLINE>
159     --boundsep--
160
161     >>> from email.mime.multipart import MIMEMultipart
162     >>> message = MIMEMultipart()
163     >>> message.attach(encodedMIMEText('Part A'))
164     >>> message.attach(encodedMIMEText('Part B'))
165     >>> encrypted = encrypt(
166     ...     message, recipients=['pgp-mime@invalid.com'], always_trust=True)
167     >>> encrypted.set_boundary('boundsep')
168     >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
169     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
170     MIME-Version: 1.0
171     Content-Disposition: inline
172     <BLANKLINE>
173     --boundsep
174     MIME-Version: 1.0
175     Content-Transfer-Encoding: 7bit
176     Content-Type: application/pgp-encrypted; charset="us-ascii"
177     <BLANKLINE>
178     Version: 1
179     <BLANKLINE>
180     --boundsep
181     MIME-Version: 1.0
182     Content-Transfer-Encoding: 7bit
183     Content-Description: OpenPGP encrypted message
184     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
185     <BLANKLINE>
186     -----BEGIN PGP MESSAGE-----
187     Version: GnuPG...
188     -----END PGP MESSAGE-----
189     <BLANKLINE>
190     --boundsep--
191     """
192     body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
193     # use email.policy.SMTP once we get Python 3.3
194     if recipients is None:
195         recipients = [email for name,email in _email_targets(message)]
196         _LOG.debug('extracted encryption recipients: {}'.format(recipients))
197     encrypted = str(_sign_and_encrypt_bytes(
198             data=body, recipients=recipients, **kwargs), 'us-ascii')
199     enc = _MIMEApplication(
200         _data=encrypted,
201         _subtype='octet-stream; name="encrypted.asc"',
202         _encoder=_encode_7or8bit)
203     enc['Content-Description'] = 'OpenPGP encrypted message'
204     enc.set_charset('us-ascii')
205     control = _MIMEApplication(
206         _data='Version: 1\n',
207         _subtype='pgp-encrypted',
208         _encoder=_encode_7or8bit)
209     control.set_charset('us-ascii')
210     msg = _MIMEMultipart(
211         'encrypted',
212         micalg='pgp-sha1',
213         protocol='application/pgp-encrypted')
214     msg.attach(control)
215     msg.attach(enc)
216     msg['Content-Disposition'] = 'inline'
217     return msg
218
219 def sign_and_encrypt(message, signers=None, recipients=None, **kwargs):
220     r"""Sign and encrypt a ``Message``, returning the encrypted version.
221
222     multipart/encrypted
223      +-> application/pgp-encrypted  (control information)
224      +-> application/octet-stream   (body)
225
226     >>> from pgp_mime.email import encodedMIMEText
227     >>> message = encodedMIMEText('Hi\nBye')
228     >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
229     >>> encrypted = sign_and_encrypt(
230     ...     message, signers=['pgp-mime@invalid.com'], always_trust=True)
231     >>> encrypted.set_boundary('boundsep')
232     >>> print(encrypted.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
233     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
234     MIME-Version: 1.0
235     Content-Disposition: inline
236     <BLANKLINE>
237     --boundsep
238     MIME-Version: 1.0
239     Content-Transfer-Encoding: 7bit
240     Content-Type: application/pgp-encrypted; charset="us-ascii"
241     <BLANKLINE>
242     Version: 1
243     <BLANKLINE>
244     --boundsep
245     MIME-Version: 1.0
246     Content-Transfer-Encoding: 7bit
247     Content-Description: OpenPGP encrypted message
248     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
249     <BLANKLINE>
250     -----BEGIN PGP MESSAGE-----
251     Version: GnuPG...
252     -----END PGP MESSAGE-----
253     <BLANKLINE>
254     --boundsep--
255
256     >>> from email.mime.multipart import MIMEMultipart
257     >>> message = MIMEMultipart()
258     >>> message.attach(encodedMIMEText('Part A'))
259     >>> message.attach(encodedMIMEText('Part B'))
260     >>> encrypted = sign_and_encrypt(
261     ...     message, signers=['pgp-mime@invalid.com'],
262     ...     recipients=['pgp-mime@invalid.com'], always_trust=True)
263     >>> encrypted.set_boundary('boundsep')
264     >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
265     Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
266     MIME-Version: 1.0
267     Content-Disposition: inline
268     <BLANKLINE>
269     --boundsep
270     MIME-Version: 1.0
271     Content-Transfer-Encoding: 7bit
272     Content-Type: application/pgp-encrypted; charset="us-ascii"
273     <BLANKLINE>
274     Version: 1
275     <BLANKLINE>
276     --boundsep
277     MIME-Version: 1.0
278     Content-Transfer-Encoding: 7bit
279     Content-Description: OpenPGP encrypted message
280     Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
281     <BLANKLINE>
282     -----BEGIN PGP MESSAGE-----
283     Version: GnuPG...
284     -----END PGP MESSAGE-----
285     <BLANKLINE>
286     --boundsep--
287     """
288     _strip_bcc(message=message)
289     body = message.as_string().encode('us-ascii').replace(b'\n', b'\r\n')
290     # use email.policy.SMTP once we get Python 3.3
291     if recipients is None:
292         recipients = [email for name,email in _email_targets(message)]
293         _LOG.debug('extracted encryption recipients: {}'.format(recipients))
294     encrypted = str(
295         _sign_and_encrypt_bytes(
296             data=body, signers=signers, recipients=recipients, **kwargs),
297         'us-ascii')
298     enc = _MIMEApplication(
299         _data=encrypted,
300         _subtype='octet-stream; name="encrypted.asc"',
301         _encoder=_encode_7or8bit)
302     enc['Content-Description'] = 'OpenPGP encrypted message'
303     enc.set_charset('us-ascii')
304     control = _MIMEApplication(
305         _data='Version: 1\n',
306         _subtype='pgp-encrypted',
307         _encoder=_encode_7or8bit)
308     control.set_charset('us-ascii')
309     msg = _MIMEMultipart(
310         'encrypted',
311         micalg='pgp-sha1',
312         protocol='application/pgp-encrypted')
313     msg.attach(control)
314     msg.attach(enc)
315     msg['Content-Disposition'] = 'inline'
316     return msg
317
318 def _get_encrypted_parts(message):
319     ct = message.get_content_type()
320     assert ct == 'multipart/encrypted', ct
321     params = dict(message.get_params())
322     assert params.get('protocol', None) == 'application/pgp-encrypted', params
323     assert message.is_multipart(), message
324     control = body = None
325     for part in message.get_payload():
326         if part == message:
327             continue
328         assert part.is_multipart() == False, part
329         ct = part.get_content_type()
330         if ct == 'application/pgp-encrypted':
331             if control:
332                 raise ValueError('multiple application/pgp-encrypted parts')
333             control = part
334         elif ct == 'application/octet-stream':
335             if body:
336                 raise ValueError('multiple application/octet-stream parts')
337             body = part
338         else:
339             raise ValueError('unnecessary {} part'.format(ct))
340     if not control:
341         raise ValueError('missing application/pgp-encrypted part')
342     if not body:
343         raise ValueError('missing application/octet-stream part')
344     return (control, body)
345
346 def _get_signed_parts(message):
347     ct = message.get_content_type()
348     assert ct == 'multipart/signed', ct
349     params = dict(message.get_params())
350     assert params.get('protocol', None) == 'application/pgp-signature', params
351     assert message.is_multipart(), message
352     body = signature = None
353     for part in message.get_payload():
354         if part == message:
355             continue
356         ct = part.get_content_type()
357         if ct == 'application/pgp-signature':
358             if signature:
359                 raise ValueError('multiple application/pgp-signature parts')
360             signature = part
361         else:
362             if body:
363                 raise ValueError('multiple non-signature parts')
364             body = part
365     if not body:
366         raise ValueError('missing body part')
367     if not signature:
368         raise ValueError('missing application/pgp-signature part')
369     return (body, signature)
370
371 def decrypt(message, **kwargs):
372     r"""Decrypt a multipart/encrypted message.
373
374     >>> from pgp_mime.email import encodedMIMEText
375     >>> message = encodedMIMEText('Hi\nBye')
376     >>> encrypted = encrypt(message, recipients=['<pgp-mime@invalid.com>'])
377     >>> decrypted = decrypt(encrypted)
378     >>> print(decrypted.as_string().replace('\r\n', '\n'))
379     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
380     Content-Type: text/plain; charset="us-ascii"
381     MIME-Version: 1.0
382     Content-Transfer-Encoding: 7bit
383     Content-Disposition: inline
384     <BLANKLINE>
385     Hi
386     Bye
387
388     >>> from email.mime.multipart import MIMEMultipart
389     >>> message = MIMEMultipart()
390     >>> message.attach(encodedMIMEText('Part A'))
391     >>> message.attach(encodedMIMEText('Part B'))
392     >>> encrypted = encrypt(
393     ...     message, recipients=['pgp-mime@invalid.com'], always_trust=True)
394     >>> decrypted = decrypt(encrypted)
395     >>> decrypted.set_boundary('boundsep')
396     >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
397     Content-Type: multipart/mixed; boundary="boundsep"
398     MIME-Version: 1.0
399     <BLANKLINE>
400     --boundsep
401     Content-Type: text/plain; charset="us-ascii"
402     MIME-Version: 1.0
403     Content-Transfer-Encoding: 7bit
404     Content-Disposition: inline
405     <BLANKLINE>
406     Part A
407     --boundsep
408     Content-Type: text/plain; charset="us-ascii"
409     MIME-Version: 1.0
410     Content-Transfer-Encoding: 7bit
411     Content-Disposition: inline
412     <BLANKLINE>
413     Part B
414     --boundsep--
415     <BLANKLINE>
416     """
417     control,body = _get_encrypted_parts(message)
418     encrypted = body.get_payload(decode=True)
419     if not isinstance(encrypted, bytes):
420         encrypted = encrypted.encode('us-ascii')
421     decrypted,verified,result = _verify_bytes(encrypted, **kwargs)
422     return _message_from_bytes(decrypted)
423
424 def verify(message, **kwargs):
425     r"""Verify a signature on ``message``, possibly decrypting first.
426
427     >>> from pgp_mime.email import encodedMIMEText
428     >>> message = encodedMIMEText('Hi\nBye')
429     >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
430     >>> encrypted = sign_and_encrypt(message, signers=['pgp-mime@invalid.com'],
431     ...     always_trust=True)
432     >>> decrypted,verified,signatures = verify(encrypted)
433     >>> print(decrypted.as_string().replace('\r\n', '\n'))
434     ... # doctest: +ELLIPSIS, +REPORT_UDIFF
435     Content-Type: text/plain; charset="us-ascii"
436     MIME-Version: 1.0
437     Content-Transfer-Encoding: 7bit
438     Content-Disposition: inline
439     To: pgp-mime-test <pgp-mime@invalid.com>
440     <BLANKLINE>
441     Hi
442     Bye
443     >>> verified
444     False
445     >>> for s in signatures:
446     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
447     ... # doctest: +REPORT_UDIFF, +ELLIPSIS
448     DECC812C8795ADD60538B0CD171008BA2F73DE2E signature:
449       summary:
450         CRL missing: False
451         CRL too old: False
452         bad policy: False
453         green: False
454         key expired: False
455         key missing: False
456         key revoked: False
457         red: False
458         signature expired: False
459         system error: False
460         valid: False
461       status: success
462       timestamp: ...
463       expiration timestamp: None
464       wrong key usage: False
465       pka trust: not available
466       chain model: False
467       validity: unknown
468       validity reason: success
469       public key algorithm: RSA
470       hash algorithm: SHA256
471
472     >>> from email.mime.multipart import MIMEMultipart
473     >>> message = MIMEMultipart()
474     >>> message.attach(encodedMIMEText('Part A'))
475     >>> message.attach(encodedMIMEText('Part B'))
476     >>> signed = sign(message, signers=['pgp-mime@invalid.com'])
477     >>> decrypted,verified,signatures = verify(signed)
478     >>> decrypted.set_boundary('boundsep')
479     >>> print(decrypted.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
480     Content-Type: multipart/mixed; boundary="boundsep"
481     MIME-Version: 1.0
482     <BLANKLINE>
483     --boundsep
484     Content-Type: text/plain; charset="us-ascii"
485     MIME-Version: 1.0
486     Content-Transfer-Encoding: 7bit
487     Content-Disposition: inline
488     <BLANKLINE>
489     Part A
490     --boundsep
491     Content-Type: text/plain; charset="us-ascii"
492     MIME-Version: 1.0
493     Content-Transfer-Encoding: 7bit
494     Content-Disposition: inline
495     <BLANKLINE>
496     Part B
497     --boundsep--
498     >>> verified
499     False
500     >>> for s in signatures:
501     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
502     ... # doctest: +REPORT_UDIFF, +ELLIPSIS
503     DECC812C8795ADD60538B0CD171008BA2F73DE2E signature:
504       summary:
505         CRL missing: False
506         CRL too old: False
507         bad policy: False
508         green: False
509         key expired: False
510         key missing: False
511         key revoked: False
512         red: False
513         signature expired: False
514         system error: False
515         valid: False
516       status: success
517       timestamp: ...
518       expiration timestamp: None
519       wrong key usage: False
520       pka trust: not available
521       chain model: False
522       validity: unknown
523       validity reason: success
524       public key algorithm: RSA
525       hash algorithm: SHA1
526
527     Test a message generated by Mutt (for sanity):
528
529     >>> from email import message_from_bytes
530     >>> message_bytes = b'\n'.join([
531     ...   b'Return-Path: <pgp-mime@invalid.com>',
532     ...   b'Received: by invalid; Tue, 24 Apr 2012 19:46:59 -0400',
533     ...   b'Date: Tue, 24 Apr 2012 19:46:59 -0400',
534     ...   b'From: pgp-mime-test <pgp-mime@invalid.com',
535     ...   b'To: pgp-mime@invalid.com',
536     ...   b'Subject: test',
537     ...   b'Message-ID: <20120424233415.GA27788@invalid>',
538     ...   b'MIME-Version: 1.0',
539     ...   b'Content-Type: multipart/signed; micalg=pgp-sha1;',
540     ...   b'  protocol="application/pgp-signature";',
541     ...   b'  boundary="kORqDWCi7qDJ0mEj"',
542     ...   b'Content-Disposition: inline',
543     ...   b'User-Agent: Mutt/1.5.21 (2010-09-15)',
544     ...   b'Content-Length: 740',
545     ...   b'',
546     ...   b'',
547     ...   b'--kORqDWCi7qDJ0mEj',
548     ...   b'Content-Type: text/plain; charset=us-ascii',
549     ...   b'Content-Disposition: inline',
550     ...   b'',
551     ...   b'ping!',
552     ...   b'',
553     ...   b'--kORqDWCi7qDJ0mEj',
554     ...   b'Content-Type: application/pgp-signature; name="signature.asc"',
555     ...   b'Content-Description: OpenPGP digital signature',
556     ...   b'',
557     ...   b'-----BEGIN PGP SIGNATURE-----',
558     ...   b'Version: GnuPG v2.0.17 (GNU/Linux)',
559     ...   b'',
560     ...   b'iQEcBAEBAgAGBQJPlztxAAoJEFEa7aZDMrbjwT0H/i9eN6CJ2FIinK7Ps04XYEbL',
561     ...   b'PSQV1xCxb+2bk7yA4zQnjAKOPSuMDXfVG669Pbj8yo4DOgUqIgh+lK+voec9uwsJ',
562     ...   b'ZgUJcMozSmEFSTPO+Fiyx0S+NjnaLsas6IQrQTVDc6lWiIZttgxuN0crH5DcLomB',
563     ...   b'Ip90+ELbzVN3yBAjMJ1Y6xnKd7C0IOKm7VunYu9eCzJ/Rik5qZ0+IacQQnnrFJEN',
564     ...   b'04nDvDUzfaKy80Ke7VAQBIRi85XCsM2h0KDXOGUZ0xPQ8L/4eUK9tL6DJaqKqFPl',
565     ...   b'zNiwfpue01o6l6kngrQdXZ3tuv0HbLGc4ACzfz5XuGvE5PYTNEsylKLUMiSCIFc=',
566     ...   b'=xP0S',
567     ...   b'-----END PGP SIGNATURE-----',
568     ...   b'',
569     ...   b'--kORqDWCi7qDJ0mEj--',
570     ...   b''])
571     >>> message = message_from_bytes(message_bytes)
572     >>> decrypted,verified,signatures = verify(message)
573     >>> print(decrypted.as_string())  # doctest: +ELLIPSIS, +REPORT_UDIFF
574     Content-Type: text/plain; charset=us-ascii
575     Content-Disposition: inline
576     <BLANKLINE>
577     ping!
578     <BLANKLINE>
579     >>> verified
580     False
581     >>> for s in signatures:
582     ...     print(s.dumps())  # doctest: +REPORT_UDIFF
583     ... # doctest: +REPORT_UDIFF, +ELLIPSIS
584     B2EDBE0E771A4B8708DD16A7511AEDA64332B6E3 signature:
585       summary:
586         CRL missing: False
587         CRL too old: False
588         bad policy: False
589         green: False
590         key expired: False
591         key missing: False
592         key revoked: False
593         red: False
594         signature expired: False
595         system error: False
596         valid: False
597       status: success
598       timestamp: Tue Apr 24 23:46:57 2012
599       expiration timestamp: None
600       wrong key usage: False
601       pka trust: not available
602       chain model: False
603       validity: unknown
604       validity reason: success
605       public key algorithm: RSA
606       hash algorithm: SHA1
607     """
608     ct = message.get_content_type()
609     if ct == 'multipart/encrypted':
610         control,body = _get_encrypted_parts(message)
611         encrypted = body.get_payload(decode=True)
612         if not isinstance(encrypted, bytes):
613             encrypted = encrypted.encode('us-ascii')
614         decrypted,verified,message = _verify_bytes(encrypted)
615         return (_message_from_bytes(decrypted), verified, message)
616     body,signature = _get_signed_parts(message)
617     sig_data = signature.get_payload(decode=True)
618     if not isinstance(sig_data, bytes):
619         sig_data = sig_data.encode('us-ascii')
620     decrypted,verified,result = _verify_bytes(
621         body.as_string().encode('us-ascii').replace(b'\n', b'\r\n'),
622         signature=sig_data, **kwargs)
623     # use email.policy.SMTP once we get Python 3.3
624     return (_copy.deepcopy(body), verified, result)