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