pgp: force protocol/micalg ordering in doctest output.
[pgp-mime.git] / pgp_mime / email.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
3 #
4 # This file is part of pgp-mime.
5 #
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
9 # version.
10 #
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.
14 #
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
18 from __future__ import absolute_import
19
20 from email.header import decode_header as _decode_header
21 from email.message import Message as _Message
22 from email.mime.text import MIMEText as _MIMEText
23 from email.parser import Parser as _Parser
24 from email.utils import formataddr as _formataddr
25 from email.utils import getaddresses as _getaddresses
26
27
28 ENCODING = 'utf-8'
29 #ENCODING = 'iso-8859-1'
30
31
32 def header_from_text(text):
33     r"""Simple wrapper for instantiating a ``Message`` from text.
34
35     >>> text = '\n'.join(
36     ...     ['From: me@big.edu','To: you@big.edu','Subject: testing'])
37     >>> header = header_from_text(text=text)
38     >>> print(header.as_string())  # doctest: +REPORT_UDIFF
39     From: me@big.edu
40     To: you@big.edu
41     Subject: testing
42     <BLANKLINE>
43     <BLANKLINE>
44     """
45     text = text.strip()
46     p = _Parser()
47     return p.parsestr(text, headersonly=True)
48
49 def guess_encoding(text):
50     r"""
51     >>> guess_encoding('hi there')
52     'us-ascii'
53     >>> guess_encoding('✉')
54     'utf-8'
55     """
56     for encoding in ['us-ascii', ENCODING, 'utf-8']:
57         try:
58             text.encode(encoding)
59         except UnicodeEncodeError:
60             pass
61         else:
62             return encoding
63     raise ValueError(text)
64
65 def encodedMIMEText(body, encoding=None):
66     """Wrap ``MIMEText`` with ``guess_encoding`` detection.
67
68     >>> message = encodedMIMEText('Hello')
69     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
70     Content-Type: text/plain; charset="us-ascii"
71     MIME-Version: 1.0
72     Content-Transfer-Encoding: 7bit
73     Content-Disposition: inline
74     <BLANKLINE>
75     Hello
76     >>> message = encodedMIMEText('Джон Доу')
77     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
78     Content-Type: text/plain; charset="utf-8"
79     MIME-Version: 1.0
80     Content-Transfer-Encoding: base64
81     Content-Disposition: inline
82     <BLANKLINE>
83     0JTQttC+0L0g0JTQvtGD
84     <BLANKLINE>
85     """
86     if encoding == None:
87         encoding = guess_encoding(body)
88     if encoding == 'us-ascii':
89         message = _MIMEText(body)
90     else:
91         # Create the message ('plain' stands for Content-Type: text/plain)
92         message = _MIMEText(body, 'plain', encoding)
93     message.add_header('Content-Disposition', 'inline')
94     return message
95
96 def strip_bcc(message):
97     """Remove the Bcc field from a ``Message`` in preparation for mailing
98
99     >>> message = encodedMIMEText('howdy!')
100     >>> message['To'] = 'John Doe <jdoe@a.gov.ru>'
101     >>> message['Bcc'] = 'Jack <jack@hill.org>, Jill <jill@hill.org>'
102     >>> message = strip_bcc(message)
103     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
104     Content-Type: text/plain; charset="us-ascii"
105     MIME-Version: 1.0
106     Content-Transfer-Encoding: 7bit
107     Content-Disposition: inline
108     To: John Doe <jdoe@a.gov.ru>
109     <BLANKLINE>
110     howdy!
111     """
112     del message['bcc']
113     del message['resent-bcc']
114     return message
115
116 def append_text(text_part, new_text):
117     r"""Append text to the body of a ``plain/text`` part.
118
119     Updates encoding as necessary.
120
121     >>> message = encodedMIMEText('Hello')
122     >>> append_text(message, ' John Doe')
123     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
124     Content-Type: text/plain; charset="us-ascii"
125     MIME-Version: 1.0
126     Content-Disposition: inline
127     Content-Transfer-Encoding: 7bit
128     <BLANKLINE>
129     Hello John Doe
130     >>> append_text(message, ', Джон Доу')
131     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
132     MIME-Version: 1.0
133     Content-Disposition: inline
134     Content-Type: text/plain; charset="utf-8"
135     Content-Transfer-Encoding: base64
136     <BLANKLINE>
137     SGVsbG8gSm9obiBEb2UsINCU0LbQvtC9INCU0L7Rgw==
138     <BLANKLINE>
139     >>> append_text(message, ', and Jane Sixpack.')
140     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
141     MIME-Version: 1.0
142     Content-Disposition: inline
143     Content-Type: text/plain; charset="utf-8"
144     Content-Transfer-Encoding: base64
145     <BLANKLINE>
146     SGVsbG8gSm9obiBEb2UsINCU0LbQvtC9INCU0L7RgywgYW5kIEphbmUgU2l4cGFjay4=
147     <BLANKLINE>
148     """
149     original_encoding = text_part.get_charset().input_charset
150     original_payload = str(
151         text_part.get_payload(decode=True), original_encoding)
152     new_payload = '{}{}'.format(original_payload, new_text)
153     new_encoding = guess_encoding(new_payload)
154     if text_part.get('content-transfer-encoding', None):
155         # clear CTE so set_payload will set it properly for the new encoding
156         del text_part['content-transfer-encoding']
157     text_part.set_payload(new_payload, new_encoding)
158
159 def attach_root(header, root_part):
160     r"""Copy headers from ``header`` onto ``root_part``.
161
162     >>> header = header_from_text('From: me@big.edu\n')
163     >>> body = encodedMIMEText('Hello')
164     >>> message = attach_root(header, body)
165     >>> print(message.as_string())  # doctest: +REPORT_UDIFF
166     Content-Type: text/plain; charset="us-ascii"
167     MIME-Version: 1.0
168     Content-Transfer-Encoding: 7bit
169     Content-Disposition: inline
170     From: me@big.edu
171     <BLANKLINE>
172     Hello
173     """
174     for k,v in header.items():
175         root_part[k] = v
176     return root_part    
177
178 def getaddresses(addresses):
179     """A decoding version of ``email.utils.getaddresses``.
180
181     >>> text = ('To: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>, '
182     ...     'Jack <jack@hill.org>')
183     >>> header = header_from_text(text=text)
184     >>> list(getaddresses(header.get_all('to', [])))
185     [('Джон Доу', 'jdoe@a.gov.ru'), ('Jack', 'jack@hill.org')]
186     """
187     for (name,address) in _getaddresses(addresses):
188         n = []
189         for b,encoding in _decode_header(name):
190             if encoding is None:
191                 n.append(b)
192             else:
193                 n.append(str(b, encoding))
194         yield (' '.join(n), address)
195
196 def email_sources(message):
197     """Extract author address from an email ``Message``
198
199     Search the header of an email Message instance to find the
200     senders' email addresses (or sender's address).
201
202     >>> text = ('From: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>, '
203     ...     'Jack <jack@hill.org>')
204     >>> header = header_from_text(text=text)
205     >>> list(email_sources(header))
206     [('Джон Доу', 'jdoe@a.gov.ru'), ('Jack', 'jack@hill.org')]
207     """
208     froms = message.get_all('from', [])
209     return getaddresses(froms) # [(name, address), ...]
210
211 def email_targets(message):
212     """Extract recipient addresses from an email ``Message``
213
214     Search the header of an email Message instance to find a
215     list of recipient's email addresses.
216
217     >>> text = ('To: =?utf-8?b?0JTQttC+0L0g0JTQvtGD?= <jdoe@a.gov.ru>, '
218     ...     'Jack <jack@hill.org>')
219     >>> header = header_from_text(text=text)
220     >>> list(email_targets(header))
221     [('Джон Доу', 'jdoe@a.gov.ru'), ('Jack', 'jack@hill.org')]
222     """
223     tos = message.get_all('to', [])
224     ccs = message.get_all('cc', [])
225     bccs = message.get_all('bcc', [])
226     resent_tos = message.get_all('resent-to', [])
227     resent_ccs = message.get_all('resent-cc', [])
228     resent_bccs = message.get_all('resent-bcc', [])
229     return getaddresses(
230         tos + ccs + bccs + resent_tos + resent_ccs + resent_bccs)