1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
4 # This file is part of pgp-mime.
6 # pgp-mime is free software: you can redistribute it and/or modify it under the
7 # terms of the GNU General Public License as published by the Free Software
8 # Foundation, either version 3 of the License, or (at your option) any later
11 # pgp-mime is distributed in the hope that it will be useful, but WITHOUT ANY
12 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along with
16 # pgp-mime. If not, see <http://www.gnu.org/licenses/>.
17 """Python module and for constructing and sending pgp/mime email.
19 Mostly uses subprocess to call ``gpg`` and sends mail using either
20 SMTP or a sendmail-compatible mailer. If you lack ``gpg``, either
21 don't use the encryption functions, adjust the ``GPG_*`` constants, or
22 adjust the ``*_bytes`` commands.
25 import configparser as _configparser
27 import logging as _logging
30 import smtplib as _smtplib
31 import smtplib as _smtplib
32 import subprocess as _subprocess
33 import threading as _threading
35 from email import message_from_bytes as _message_from_bytes
36 from email.encoders import encode_7or8bit as _encode_7or8bit
37 from email.header import decode_header as _decode_header
38 from email.message import Message as _Message
39 from email.mime.application import MIMEApplication as _MIMEApplication
40 from email.mime.multipart import MIMEMultipart as _MIMEMultipart
41 from email.mime.text import MIMEText as _MIMEText
42 from email.parser import Parser as _Parser
43 from email.utils import formataddr as _formataddr
44 from email.utils import getaddresses as _getaddresses
50 LOG = _logging.getLogger('pgp-mime')
51 LOG.setLevel(_logging.ERROR)
52 LOG.addHandler(_logging.StreamHandler())
55 #ENCODING = 'iso-8859-1'
58 '/usr/bin/gpg', '--no-verbose', '--quiet', '--batch', '--output', '-']
59 GPG_SIGN_ARGS = ['--armor', '--textmode', '--detach-sign']
60 GPG_ENCRYPT_ARGS = ['--armor', '--textmode', '--encrypt', '--always-trust']
61 GPG_SIGN_AND_ENCRYPT_ARGS = [
62 '--armor', '--textmode', '--sign', '--encrypt', '--always-trust']
66 'This key is not certified with a trusted signature',
69 SENDMAIL = ['/usr/sbin/sendmail', '-t']
72 def get_smtp_params(config):
73 r"""Retrieve SMTP paramters from a config file.
75 >>> from configparser import ConfigParser
76 >>> config = ConfigParser()
77 >>> config.read_string('\n'.join([
79 ... 'host: smtp.mail.uu.edu',
82 ... 'username: rincewind',
83 ... 'password: 7ugg@g3',
85 >>> get_smtp_params(config)
86 ('smtp.mail.uu.edu', 587, True, 'rincewind', '7ugg@g3')
87 >>> config = ConfigParser()
88 >>> get_smtp_params(ConfigParser())
89 (None, None, None, None, None)
92 host = config.get('smtp', 'host')
93 except _configparser.NoSectionError:
94 return (None, None, None, None, None)
95 except _configparser.NoOptionError:
98 port = config.getint('smtp', 'port')
99 except _configparser.NoOptionError:
102 starttls = config.getboolean('smtp', 'starttls')
103 except _configparser.NoOptionError:
106 username = config.get('smtp', 'username')
107 except _configparser.NoOptionError:
110 password = config.get('smtp', 'password')
111 except _configparser.NoOptionError:
113 return (host, port, starttls, username, password)
115 def get_smtp(host=None, port=None, starttls=None, username=None,
117 """Connect to an SMTP host using the given parameters.
120 >>> try: # doctest: +SKIP
121 ... smtp = get_smtp(host='smtp.gmail.com', port=587, starttls=True,
122 ... username='rincewind@uu.edu', password='7ugg@g3')
123 ... except smtplib.SMTPAuthenticationError as error:
124 ... print('that was not a real account')
125 that was not a real account
126 >>> smtp = get_smtp() # doctest: +SKIP
127 >>> smtp.quit() # doctest: +SKIP
132 port = _smtplib.SMTP_PORT
133 if username and not starttls:
135 'sending passwords in the clear is unsafe! Use STARTTLS.')
136 LOG.info('connect to SMTP server at {}:{}'.format(host, port))
137 smtp = _smtplib.SMTP(host=host, port=port)
142 smtp.login(username, password)
143 #smtp.set_debuglevel(1)
146 def mail(message, smtp=None, sendmail=None):
147 """Send an email ``Message`` instance on its merry way.
149 We can shell out to the user specified sendmail in case
150 the local host doesn't have an SMTP server set up
151 for easy ``smtplib`` usage.
153 >>> message = encodedMIMEText('howdy!')
154 >>> message['From'] = 'John Doe <jdoe@a.gov.ru>'
155 >>> message['To'] = 'Jack <jack@hill.org>, Jill <jill@hill.org>'
156 >>> mail(message=message, sendmail=SENDMAIL) # doctest: +SKIP
158 LOG.info('send message {} -> {}'.format(message['from'], message['to']))
160 smtp.send_message(msg=message)
163 sendmail, stdin=message.as_string().encode('us-ascii'),
166 smtp = _smtplib.SMTP()
168 smtp.send_message(msg=message)
171 def header_from_text(text):
172 r"""Simple wrapper for instantiating a ``Message`` from text.
174 >>> text = '\n'.join(
175 ... ['From: me@big.edu','To: you@big.edu','Subject: testing'])
176 >>> header = header_from_text(text=text)
177 >>> print(header.as_string()) # doctest: +REPORT_UDIFF
186 return p.parsestr(text, headersonly=True)
188 def guess_encoding(text):
190 >>> guess_encoding('hi there')
192 >>> guess_encoding('✉')
195 for encoding in ['us-ascii', ENCODING, 'utf-8']:
197 text.encode(encoding)
198 except UnicodeEncodeError:
202 raise ValueError(text)
204 def encodedMIMEText(body, encoding=None):
205 """Wrap ``MIMEText`` with ``guess_encoding`` detection.
207 >>> message = encodedMIMEText('Hello')
208 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
209 Content-Type: text/plain; charset="us-ascii"
211 Content-Transfer-Encoding: 7bit
212 Content-Disposition: inline
215 >>> message = encodedMIMEText('Джон Доу')
216 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
217 Content-Type: text/plain; charset="utf-8"
219 Content-Transfer-Encoding: base64
220 Content-Disposition: inline
226 encoding = guess_encoding(body)
227 if encoding == 'us-ascii':
228 message = _MIMEText(body)
230 # Create the message ('plain' stands for Content-Type: text/plain)
231 message = _MIMEText(body, 'plain', encoding)
232 message.add_header('Content-Disposition', 'inline')
235 def strip_bcc(message):
236 """Remove the Bcc field from a ``Message`` in preparation for mailing
238 >>> message = encodedMIMEText('howdy!')
239 >>> message['To'] = 'John Doe <jdoe@a.gov.ru>'
240 >>> message['Bcc'] = 'Jack <jack@hill.org>, Jill <jill@hill.org>'
241 >>> message = strip_bcc(message)
242 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
243 Content-Type: text/plain; charset="us-ascii"
245 Content-Transfer-Encoding: 7bit
246 Content-Disposition: inline
247 To: John Doe <jdoe@a.gov.ru>
252 del message['resent-bcc']
255 def append_text(text_part, new_text):
256 r"""Append text to the body of a ``plain/text`` part.
258 Updates encoding as necessary.
260 >>> message = encodedMIMEText('Hello')
261 >>> append_text(message, ' John Doe')
262 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
263 Content-Type: text/plain; charset="us-ascii"
265 Content-Disposition: inline
266 Content-Transfer-Encoding: 7bit
269 >>> append_text(message, ', Джон Доу')
270 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
272 Content-Disposition: inline
273 Content-Type: text/plain; charset="utf-8"
274 Content-Transfer-Encoding: base64
276 SGVsbG8gSm9obiBEb2UsINCU0LbQvtC9INCU0L7Rgw==
278 >>> append_text(message, ', and Jane Sixpack.')
279 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
281 Content-Disposition: inline
282 Content-Type: text/plain; charset="utf-8"
283 Content-Transfer-Encoding: base64
285 SGVsbG8gSm9obiBEb2UsINCU0LbQvtC9INCU0L7RgywgYW5kIEphbmUgU2l4cGFjay4=
288 original_encoding = text_part.get_charset().input_charset
289 original_payload = str(
290 text_part.get_payload(decode=True), original_encoding)
291 new_payload = '{}{}'.format(original_payload, new_text)
292 new_encoding = guess_encoding(new_payload)
293 if text_part.get('content-transfer-encoding', None):
294 # clear CTE so set_payload will set it properly for the new encoding
295 del text_part['content-transfer-encoding']
296 text_part.set_payload(new_payload, new_encoding)
298 def attach_root(header, root_part):
299 r"""Copy headers from ``header`` onto ``root_part``.
301 >>> header = header_from_text('From: me@big.edu\n')
302 >>> body = encodedMIMEText('Hello')
303 >>> message = attach_root(header, body)
304 >>> print(message.as_string()) # doctest: +REPORT_UDIFF
305 Content-Type: text/plain; charset="us-ascii"
307 Content-Transfer-Encoding: 7bit
308 Content-Disposition: inline
313 for k,v in header.items():
317 def execute(args, stdin=None, expect=(0,), env=_os.environ, **kwargs):
318 """Execute a command (allows us to drive gpg).
320 LOG.debug('$ {}'.format(args))
322 p = _subprocess.Popen(
323 args, stdin=_subprocess.PIPE, stdout=_subprocess.PIPE,
324 stderr=_subprocess.PIPE, shell=False, env=env, **kwargs)
326 raise Exception('{}\nwhile executing {}'.format(e.args[1], args))
327 output,error = p.communicate(input=stdin)
329 LOG.debug('(status: {})\n{}{}'.format(status, output, error))
330 if status not in expect:
331 raise Exception('unexpected status while executing {}\n{}\n{}'.format(
332 args, error, status))
333 return (status, output, error)
335 def getaddresses(addresses):
336 """A decoding version of ``email.utils.getaddresses``.
338 >>> text = ('To: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>, '
339 ... 'Jack <jack@hill.org>')
340 >>> header = header_from_text(text=text)
341 >>> list(getaddresses(header.get_all('to', [])))
342 [('Джон Доу', 'jdoe@a.gov.ru'), ('Jack', 'jack@hill.org')]
344 for (name,address) in _getaddresses(addresses):
346 for b,encoding in _decode_header(name):
350 n.append(str(b, encoding))
351 yield (' '.join(n), address)
353 def email_sources(message):
354 """Extract author address from an email ``Message``
356 Search the header of an email Message instance to find the
357 senders' email addresses (or sender's address).
359 >>> text = ('From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>, '
360 ... 'Jack <jack@hill.org>')
361 >>> header = header_from_text(text=text)
362 >>> list(email_sources(header))
363 [('Джон Доу', 'jdoe@a.gov.ru'), ('Jack', 'jack@hill.org')]
365 froms = message.get_all('from', [])
366 return getaddresses(froms) # [(name, address), ...]
368 def email_targets(message):
369 """Extract recipient addresses from an email ``Message``
371 Search the header of an email Message instance to find a
372 list of recipient's email addresses.
374 >>> text = ('To: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>, '
375 ... 'Jack <jack@hill.org>')
376 >>> header = header_from_text(text=text)
377 >>> list(email_targets(header))
378 [('Джон Доу', 'jdoe@a.gov.ru'), ('Jack', 'jack@hill.org')]
380 tos = message.get_all('to', [])
381 ccs = message.get_all('cc', [])
382 bccs = message.get_all('bcc', [])
383 resent_tos = message.get_all('resent-to', [])
384 resent_ccs = message.get_all('resent-cc', [])
385 resent_bccs = message.get_all('resent-bcc', [])
387 tos + ccs + bccs + resent_tos + resent_ccs + resent_bccs)
389 def _thread_pipe(fd, data):
390 """Write ``data`` to ``fd`` and close ``fd``.
392 A helper function for ``thread_pipe``.
396 LOG.debug('starting pipe-write thread')
398 remaining = len(data)
400 remaining -= _os.write(fd, data[-remaining:])
402 LOG.debug('closing pipe-write file descriptor')
404 LOG.debug('closed pipe-write file descriptor')
406 def thread_pipe(data):
407 """Write data to a pipe.
409 Return the associated read file descriptor and running ``Thread``
410 that's doing the writing.
413 >>> read,thread = thread_pipe(b'Hello world!')
415 ... print(os.read(read, 100))
420 read,write = _os.pipe()
421 LOG.debug('opened a pipe {} -> {}'.format(write, read))
423 thread = _threading.Thread(
424 name='pipe writer', target=_thread_pipe, args=(write, data))
429 return (read, thread)
431 def sign_bytes(bytes, sign_as=None):
432 r"""Sign ``bytes`` as ``sign_as``.
434 >>> print(sign_bytes(bytes(b'Hello'), 'pgp-mime@invalid.com'))
435 ... # doctest: +ELLIPSIS
436 b'-----BEGIN PGP SIGNATURE-----\n...-----END PGP SIGNATURE-----\n'
438 args = GPG_ARGS + GPG_SIGN_ARGS
440 args.extend(['--local-user', sign_as])
441 status,output,error = execute(args, stdin=bytes, close_fds=True)
444 def encrypt_bytes(bytes, recipients):
445 r"""Encrypt ``bytes`` to ``recipients``.
447 >>> encrypt_bytes(bytes(b'Hello'), ['pgp-mime@invalid.com'])
448 ... # doctest: +ELLIPSIS
449 b'-----BEGIN PGP MESSAGE-----\n...-----END PGP MESSAGE-----\n'
451 args = GPG_ARGS + GPG_ENCRYPT_ARGS
453 raise ValueError('no recipients specified for encryption')
454 for recipient in recipients:
455 args.extend(['--recipient', recipient])
456 status,output,error = execute(args, stdin=bytes, close_fds=True)
459 def sign_and_encrypt_bytes(bytes, sign_as=None, recipients=None):
460 r"""Sign ``bytes`` as ``sign_as`` and encrypt to ``recipients``.
462 >>> sign_and_encrypt_bytes(
463 ... bytes(b'Hello'), 'pgp-mime@invalid.com', ['pgp-mime@invalid.com'])
464 ... # doctest: +ELLIPSIS
465 b'-----BEGIN PGP MESSAGE-----\n...-----END PGP MESSAGE-----\n'
467 args = GPG_ARGS + GPG_SIGN_AND_ENCRYPT_ARGS
469 args.extend(['--local-user', sign_as])
471 raise ValueError('no recipients specified for encryption')
472 for recipient in recipients:
473 args.extend(['--recipient', recipient])
474 status,output,error = execute(args, stdin=bytes, close_fds=True)
477 def decrypt_bytes(bytes):
478 r"""Decrypt ``bytes``.
481 ... '-----BEGIN PGP MESSAGE-----',
482 ... 'Version: GnuPG v2.0.17 (GNU/Linux)',
484 ... 'hQEMA1Ea7aZDMrbjAQf/TAqLjksZSJxSqkBxYT5gtLQoXY6isvRZg2apjs7CW0y2',
485 ... 'tFK/ptnVYAq2OtWQFhbiJXj8hmwJyyFfb3lghpeu4ihO52JgkkwOpmJb6dxjOi83',
486 ... 'qDwaGOogEPH38BNLuwdrMCW0jmNROwvS796PtqSGUaJTuIiKUB8lETwPwIHrDc11',
487 ... 'N3RWStE5uShNkXXQXplUoeCKf3N4XguXym+GQCqJQzlEMrkkDdr4l7mzvt3Nf8EA',
488 ... 'SgSak086tUoo9x8IN5PJCuOJkcXcjQzFcpqOsA7dyZKO8NeQUZv2JvlZuorckNvN',
489 ... 'xx3PwW0a8VeJgTQrh64ZK/d3F3gNHUTzXkq/UIn25tJFAcmSUwxtsBal7p8zAeCV',
490 ... '8zefsHRQ5Y03IBeYBcVJBhDS9XfvwLQTJiGGstPCxzKTwSUT1MzV5t5twG/STDCc',
493 ... '-----END PGP MESSAGE-----',
495 ... ]).encode('us-ascii')
499 args = GPG_ARGS + GPG_DECRYPT_ARGS
500 status,output,error = execute(args, stdin=bytes, close_fds=True)
503 def verify_bytes(bytes, signature=None):
504 r"""Verify a signature on ``bytes``, possibly decrypting first.
506 These tests assume you didn't trust the distributed test key.
509 ... '-----BEGIN PGP MESSAGE-----',
510 ... 'Version: GnuPG v2.0.17 (GNU/Linux)',
512 ... 'hQEMA1Ea7aZDMrbjAQf/YM1SeFzNGz0DnUynaEyhfGCvcqmjtbN1PtZMpT7VaQLN',
513 ... 'a+c0faskr79Atz0+2IBR7CDOlcETrRtH2EnrWukbRIDtmffNFGuhMRTNfnQ15OIN',
514 ... 'qrmt2P5gXznsgnm2XjzTK7S/Cc3Aq+zjaDrDt7bIedEdz+EyNgaKuL/lB9cAB8xL',
515 ... 'YYp/yn55Myjair2idgzsa7w/QXdE3RhpyRLqR2Jgz4P1I1xOgUYnylbpIZL9FOKN',
516 ... 'NR3RQhkGdANBku8otfthb5ZUGsNMV45ct4V8PE+xChjFb9gcwpaf1hhoIF/sYHD5',
517 ... 'Bkf+v/J8F40KGYY16b0DjQIUlnra9y7q9jj0h2bvc9LAtgHtVUso133LLcVYl7RP',
518 ... 'Vjyz9Ps366BtIdPlAL4CoF5hEcMKS5J3h1vRlyAKN4uHENl5vKvoxn7ID3JhhWQc',
519 ... '6QrPGis64zi3OnYor34HPh/KNJvkgOQkekmtYuTxnkiONA4lhMDJgeaVZ9WZq+GV',
520 ... 'MaCvCFGNYU2TV4V8wMlnUbF8d5bDQ83g8MxIVKdDcnBzzYLZha+qmz4Spry9iB53',
521 ... 'Sg/sM5H8gWWSl7Oj1lxVg7o7IscpQfVt6zL6jD2VjL3L3Hu7WEXIrcGZtvrP4d+C',
522 ... 'TGYWiGlh5B2UCFk2bVctfw8W/QfaVvJYD4Rfqta2V2p14KIJLFRSGa1g26W4ixrH',
523 ... 'XKxgaA3AIfJ+6c5RoisRLuYCxvQi91wkE9hAXR+inXK4Hq4SmiHoeITZFhHP3hh3',
524 ... 'rbpp8mopiMNxWqCbuqgILP6pShn4oPclu9aR8uJ1ziDxISTGYC71mvLUERUjFn2L',
525 ... 'fu6C0+TCC9RmeyL+eNdM6cjs1G7YR6yX',
527 ... '-----END PGP MESSAGE-----',
529 ... ]).encode('us-ascii')
530 >>> output,verified,message = verify_bytes(b)
536 gpg: Signature made Wed 21 Mar 2012 03:13:57 PM EDT using RSA key ID 4332B6E3
537 gpg: Good signature from "pgp-mime-test (http://blog.tremily.us/posts/pgp-mime/) <pgp-mime@invalid.com>"
538 gpg: WARNING: This key is not certified with a trusted signature!
539 gpg: There is no indication that the signature belongs to the owner.
540 Primary key fingerprint: B2ED BE0E 771A 4B87 08DD 16A7 511A EDA6 4332 B6E3
543 >>> b = b'Success!\n'
544 >>> signature = '\n'.join([
545 ... '-----BEGIN PGP SIGNATURE-----',
546 ... 'Version: GnuPG v2.0.17 (GNU/Linux)',
548 ... 'iQEcBAEBAgAGBQJPaiw/AAoJEFEa7aZDMrbj93gH/1fQPXLjUTpONJUTmvGoMLNA',
549 ... 'W9ZhjpUL5i6rRqYGUvQ4kTEDuPMxkMrCyFCDHEhSDHufMek6Nso5/HeJn3aqxlgs',
550 ... 'hmNlvAq4FI6JQyFL7eCp/XG9cPx1p42dTI7JAih8FuK21sS4m/H5XP3R/6KXC99D',
551 ... '39rrXCvvR+yNgKe2dxuJwmKuLteVlcWxiIQwVrYK70GtJHC5BO79G8yGccWoEy9C',
552 ... '9JkJiyNptqZyFjGBNmMmrCSFZ7ZFA02RB+laRmwuIiozw4TJYEksxPrgZMbbcFzx',
553 ... 'zs3JHyV23+Fz1ftalvwskHE7tJkX9Ub8iBMNZ/KxJXXdPdpuMdEYVjoUehkQBQE=',
555 ... '-----END PGP SIGNATURE-----',
557 ... ]).encode('us-ascii')
558 >>> output,verified,message = verify_bytes(b, signature=signature)
564 gpg: Signature made Wed 21 Mar 2012 03:30:07 PM EDT using RSA key ID 4332B6E3
565 gpg: Good signature from "pgp-mime-test (http://blog.tremily.us/posts/pgp-mime/) <pgp-mime@invalid.com>"
566 gpg: WARNING: This key is not certified with a trusted signature!
567 gpg: There is no indication that the signature belongs to the owner.
568 Primary key fingerprint: B2ED BE0E 771A 4B87 08DD 16A7 511A EDA6 4332 B6E3
571 args = GPG_ARGS + GPG_VERIFY_ARGS
573 sig_read = sig_thread = None
575 sig_read,sig_thread = thread_pipe(signature)
577 ['--enable-special-filenames', '--verify',
578 '--', '-&{}'.format(sig_read), '-'])
579 kwargs['close_fds'] = False
581 kwargs['close_fds'] = True
583 status,output,error = execute(args, stdin=bytes, **kwargs)
590 assert output == b'', output
592 error = str(error, 'us-ascii')
594 for string in GPG_VERIFY_FAILED:
598 return (output, verified, error)
600 def sign(message, sign_as=None):
601 r"""Sign a ``Message``, returning the signed version.
604 +-> text/plain (body)
605 +-> application/pgp-signature (signature)
607 >>> message = encodedMIMEText('Hi\nBye')
608 >>> signed = sign(message, sign_as='pgp-mime@invalid.com')
609 >>> signed.set_boundary('boundsep')
610 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
611 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
613 Content-Disposition: inline
616 Content-Type: text/plain; charset="us-ascii"
618 Content-Transfer-Encoding: 7bit
619 Content-Disposition: inline
625 Content-Transfer-Encoding: 7bit
626 Content-Description: OpenPGP digital signature
627 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
629 -----BEGIN PGP SIGNATURE-----
631 -----END PGP SIGNATURE-----
635 >>> from email.mime.multipart import MIMEMultipart
636 >>> message = MIMEMultipart()
637 >>> message.attach(encodedMIMEText('Part A'))
638 >>> message.attach(encodedMIMEText('Part B'))
639 >>> signed = sign(message, sign_as='pgp-mime@invalid.com')
640 >>> signed.set_boundary('boundsep')
641 >>> print(signed.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
642 Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="boundsep"
644 Content-Disposition: inline
647 Content-Type: multipart/mixed; boundary="===============...=="
650 --===============...==
651 Content-Type: text/plain; charset="us-ascii"
653 Content-Transfer-Encoding: 7bit
654 Content-Disposition: inline
657 --===============...==
658 Content-Type: text/plain; charset="us-ascii"
660 Content-Transfer-Encoding: 7bit
661 Content-Disposition: inline
664 --===============...==--
667 Content-Transfer-Encoding: 7bit
668 Content-Description: OpenPGP digital signature
669 Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
671 -----BEGIN PGP SIGNATURE-----
673 -----END PGP SIGNATURE-----
677 body = message.as_string().encode('us-ascii')
678 signature = str(sign_bytes(body, sign_as), 'us-ascii')
679 sig = _MIMEApplication(
681 _subtype='pgp-signature; name="signature.asc"',
682 _encoder=_encode_7or8bit)
683 sig['Content-Description'] = 'OpenPGP digital signature'
684 sig.set_charset('us-ascii')
686 msg = _MIMEMultipart(
687 'signed', micalg='pgp-sha1', protocol='application/pgp-signature')
690 msg['Content-Disposition'] = 'inline'
693 def encrypt(message, recipients=None):
694 r"""Encrypt a ``Message``, returning the encrypted version.
697 +-> application/pgp-encrypted (control information)
698 +-> application/octet-stream (body)
700 >>> message = encodedMIMEText('Hi\nBye')
701 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
702 >>> encrypted = encrypt(message)
703 >>> encrypted.set_boundary('boundsep')
704 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
705 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
707 Content-Disposition: inline
711 Content-Transfer-Encoding: 7bit
712 Content-Type: application/pgp-encrypted; charset="us-ascii"
718 Content-Transfer-Encoding: 7bit
719 Content-Description: OpenPGP encrypted message
720 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
722 -----BEGIN PGP MESSAGE-----
724 -----END PGP MESSAGE-----
728 >>> from email.mime.multipart import MIMEMultipart
729 >>> message = MIMEMultipart()
730 >>> message.attach(encodedMIMEText('Part A'))
731 >>> message.attach(encodedMIMEText('Part B'))
732 >>> encrypted = encrypt(message, recipients=['pgp-mime@invalid.com'])
733 >>> encrypted.set_boundary('boundsep')
734 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
735 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
737 Content-Disposition: inline
741 Content-Transfer-Encoding: 7bit
742 Content-Type: application/pgp-encrypted; charset="us-ascii"
748 Content-Transfer-Encoding: 7bit
749 Content-Description: OpenPGP encrypted message
750 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
752 -----BEGIN PGP MESSAGE-----
754 -----END PGP MESSAGE-----
758 body = message.as_string().encode('us-ascii')
759 if recipients is None:
760 recipients = [email for name,email in email_targets(message)]
761 LOG.debug('extracted encryption recipients: {}'.format(recipients))
762 encrypted = str(encrypt_bytes(body, recipients), 'us-ascii')
763 enc = _MIMEApplication(
765 _subtype='octet-stream; name="encrypted.asc"',
766 _encoder=_encode_7or8bit)
767 enc['Content-Description'] = 'OpenPGP encrypted message'
768 enc.set_charset('us-ascii')
769 control = _MIMEApplication(
770 _data='Version: 1\n',
771 _subtype='pgp-encrypted',
772 _encoder=_encode_7or8bit)
773 control.set_charset('us-ascii')
774 msg = _MIMEMultipart(
777 protocol='application/pgp-encrypted')
780 msg['Content-Disposition'] = 'inline'
783 def sign_and_encrypt(message, sign_as=None, recipients=None):
784 r"""Sign and encrypt a ``Message``, returning the encrypted version.
787 +-> application/pgp-encrypted (control information)
788 +-> application/octet-stream (body)
790 >>> message = encodedMIMEText('Hi\nBye')
791 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
792 >>> encrypted = sign_and_encrypt(message, sign_as='pgp-mime@invalid.com')
793 >>> encrypted.set_boundary('boundsep')
794 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
795 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
797 Content-Disposition: inline
801 Content-Transfer-Encoding: 7bit
802 Content-Type: application/pgp-encrypted; charset="us-ascii"
808 Content-Transfer-Encoding: 7bit
809 Content-Description: OpenPGP encrypted message
810 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
812 -----BEGIN PGP MESSAGE-----
814 -----END PGP MESSAGE-----
818 >>> from email.mime.multipart import MIMEMultipart
819 >>> message = MIMEMultipart()
820 >>> message.attach(encodedMIMEText('Part A'))
821 >>> message.attach(encodedMIMEText('Part B'))
822 >>> encrypted = sign_and_encrypt(
823 ... message, sign_as='pgp-mime@invalid.com', recipients=['pgp-mime@invalid.com'])
824 >>> encrypted.set_boundary('boundsep')
825 >>> print(encrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
826 Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="boundsep"
828 Content-Disposition: inline
832 Content-Transfer-Encoding: 7bit
833 Content-Type: application/pgp-encrypted; charset="us-ascii"
839 Content-Transfer-Encoding: 7bit
840 Content-Description: OpenPGP encrypted message
841 Content-Type: application/octet-stream; name="encrypted.asc"; charset="us-ascii"
843 -----BEGIN PGP MESSAGE-----
845 -----END PGP MESSAGE-----
849 strip_bcc(message=message)
850 body = message.as_string().encode('us-ascii')
851 if recipients is None:
852 recipients = [email for name,email in email_targets(message)]
853 LOG.debug('extracted encryption recipients: {}'.format(recipients))
854 encrypted = str(sign_and_encrypt_bytes(
855 body, sign_as=sign_as, recipients=recipients), 'us-ascii')
856 enc = _MIMEApplication(
858 _subtype='octet-stream; name="encrypted.asc"',
859 _encoder=_encode_7or8bit)
860 enc['Content-Description'] = 'OpenPGP encrypted message'
861 enc.set_charset('us-ascii')
862 control = _MIMEApplication(
863 _data='Version: 1\n',
864 _subtype='pgp-encrypted',
865 _encoder=_encode_7or8bit)
866 control.set_charset('us-ascii')
867 msg = _MIMEMultipart(
870 protocol='application/pgp-encrypted')
873 msg['Content-Disposition'] = 'inline'
876 def _get_encrypted_parts(message):
877 ct = message.get_content_type()
878 assert ct == 'multipart/encrypted', ct
879 params = dict(message.get_params())
880 assert params.get('protocol', None) == 'application/pgp-encrypted', params
881 assert message.is_multipart(), message
882 control = body = None
883 for part in message.get_payload():
886 assert part.is_multipart() == False, part
887 ct = part.get_content_type()
888 if ct == 'application/pgp-encrypted':
890 raise ValueError('multiple application/pgp-encrypted parts')
892 elif ct == 'application/octet-stream':
894 raise ValueError('multiple application/octet-stream parts')
897 raise ValueError('unnecessary {} part'.format(ct))
899 raise ValueError('missing application/pgp-encrypted part')
901 raise ValueError('missing application/octet-stream part')
902 return (control, body)
904 def _get_signed_parts(message):
905 ct = message.get_content_type()
906 assert ct == 'multipart/signed', ct
907 params = dict(message.get_params())
908 assert params.get('protocol', None) == 'application/pgp-signature', params
909 assert message.is_multipart(), message
910 body = signature = None
911 for part in message.get_payload():
914 ct = part.get_content_type()
915 if ct == 'application/pgp-signature':
917 raise ValueError('multiple application/pgp-signature parts')
921 raise ValueError('multiple non-signature parts')
924 raise ValueError('missing body part')
926 raise ValueError('missing application/pgp-signature part')
927 return (body, signature)
929 def decrypt(message):
930 r"""Decrypt a multipart/encrypted message.
932 >>> message = encodedMIMEText('Hi\nBye')
933 >>> encrypted = encrypt(message, recipients=['<pgp-mime@invalid.com>'])
934 >>> decrypted = decrypt(encrypted)
935 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
936 Content-Type: text/plain; charset="us-ascii"
938 Content-Transfer-Encoding: 7bit
939 Content-Disposition: inline
944 >>> from email.mime.multipart import MIMEMultipart
945 >>> message = MIMEMultipart()
946 >>> message.attach(encodedMIMEText('Part A'))
947 >>> message.attach(encodedMIMEText('Part B'))
948 >>> encrypted = encrypt(message, recipients=['pgp-mime@invalid.com'])
949 >>> decrypted = decrypt(encrypted)
950 >>> decrypted.set_boundary('boundsep')
951 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
952 Content-Type: multipart/mixed; boundary="boundsep"
956 Content-Type: text/plain; charset="us-ascii"
958 Content-Transfer-Encoding: 7bit
959 Content-Disposition: inline
963 Content-Type: text/plain; charset="us-ascii"
965 Content-Transfer-Encoding: 7bit
966 Content-Disposition: inline
972 control,body = _get_encrypted_parts(message)
973 encrypted = body.get_payload(decode=True)
974 if not isinstance(encrypted, bytes):
975 encrypted = encrypted.encode('us-ascii')
976 decrypted = decrypt_bytes(encrypted)
977 return _message_from_bytes(decrypted)
980 r"""Verify a signature on ``message``, possibly decrypting first.
982 >>> message = encodedMIMEText('Hi\nBye')
983 >>> message['To'] = 'pgp-mime-test <pgp-mime@invalid.com>'
984 >>> encrypted = sign_and_encrypt(message, sign_as='pgp-mime@invalid.com')
985 >>> decrypted,verified,message = verify(encrypted)
986 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
987 Content-Type: text/plain; charset="us-ascii"
989 Content-Transfer-Encoding: 7bit
990 Content-Disposition: inline
991 To: pgp-mime-test <pgp-mime@invalid.com>
997 >>> print(message) # doctest: +ELLIPSIS, +REPORT_UDIFF
998 gpg: Signature made ... using RSA key ID 4332B6E3
999 gpg: Good signature from "pgp-mime-test (http://blog.tremily.us/posts/pgp-mime/) <pgp-mime@invalid.com>"
1000 gpg: WARNING: This key is not certified with a trusted signature!
1001 gpg: There is no indication that the signature belongs to the owner.
1002 Primary key fingerprint: B2ED BE0E 771A 4B87 08DD 16A7 511A EDA6 4332 B6E3
1005 >>> from email.mime.multipart import MIMEMultipart
1006 >>> message = MIMEMultipart()
1007 >>> message.attach(encodedMIMEText('Part A'))
1008 >>> message.attach(encodedMIMEText('Part B'))
1009 >>> signed = sign(message, sign_as='pgp-mime@invalid.com')
1010 >>> decrypted,verified,message = verify(signed)
1011 >>> decrypted.set_boundary('boundsep')
1012 >>> print(decrypted.as_string()) # doctest: +ELLIPSIS, +REPORT_UDIFF
1013 Content-Type: multipart/mixed; boundary="boundsep"
1017 Content-Type: text/plain; charset="us-ascii"
1019 Content-Transfer-Encoding: 7bit
1020 Content-Disposition: inline
1024 Content-Type: text/plain; charset="us-ascii"
1026 Content-Transfer-Encoding: 7bit
1027 Content-Disposition: inline
1033 >>> print(message) # doctest: +ELLIPSIS, +REPORT_UDIFF
1034 gpg: Signature made ... using RSA key ID 4332B6E3
1035 gpg: Good signature from "pgp-mime-test (http://blog.tremily.us/posts/pgp-mime/) <pgp-mime@invalid.com>"
1036 gpg: WARNING: This key is not certified with a trusted signature!
1037 gpg: There is no indication that the signature belongs to the owner.
1038 Primary key fingerprint: B2ED BE0E 771A 4B87 08DD 16A7 511A EDA6 4332 B6E3
1041 ct = message.get_content_type()
1042 if ct == 'multipart/encrypted':
1043 control,body = _get_encrypted_parts(message)
1044 encrypted = body.get_payload(decode=True)
1045 if not isinstance(encrypted, bytes):
1046 encrypted = encrypted.encode('us-ascii')
1047 decrypted,verified,message = verify_bytes(encrypted)
1048 return (_message_from_bytes(decrypted), verified, message)
1049 body,signature = _get_signed_parts(message)
1050 sig_data = signature.get_payload(decode=True)
1051 if not isinstance(sig_data, bytes):
1052 sig_data = sig_data.encode('us-ascii')
1053 decrypted,verified,message = verify_bytes(
1054 body.as_string().encode('us-ascii'), signature=sig_data)
1055 return (body, verified, message)