bzr branch
http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
1  | 
# Copyright (C) 2007 Canonical Ltd
 | 
2  | 
#
 | 
|
3  | 
# This program is free software; you can redistribute it and/or modify
 | 
|
4  | 
# it under the terms of the GNU General Public License as published by
 | 
|
5  | 
# the Free Software Foundation; either version 2 of the License, or
 | 
|
6  | 
# (at your option) any later version.
 | 
|
7  | 
#
 | 
|
8  | 
# This program is distributed in the hope that it will be useful,
 | 
|
9  | 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
|
10  | 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
|
11  | 
# GNU General Public License for more details.
 | 
|
12  | 
#
 | 
|
13  | 
# You should have received a copy of the GNU General Public License
 | 
|
14  | 
# along with this program; if not, write to the Free Software
 | 
|
15  | 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 | 
|
16  | 
||
17  | 
"""A convenience class around email.Message and email.MIMEMultipart."""
 | 
|
18  | 
||
19  | 
from email import (  | 
|
20  | 
Header,  | 
|
21  | 
Message,  | 
|
22  | 
MIMEMultipart,  | 
|
23  | 
MIMEText,  | 
|
24  | 
Utils,  | 
|
25  | 
    )
 | 
|
26  | 
||
27  | 
from bzrlib import __version__ as _bzrlib_version  | 
|
28  | 
from bzrlib.osutils import safe_unicode  | 
|
29  | 
from bzrlib.smtp_connection import SMTPConnection  | 
|
30  | 
||
31  | 
||
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
32  | 
class EmailMessage(object):  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
33  | 
"""An email message.  | 
| 
3943.8.1
by Marius Kruger
 remove all trailing whitespace from bzr source  | 
34  | 
|
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
35  | 
    The constructor needs an origin address, a destination address or addresses
 | 
36  | 
    and a subject, and accepts a body as well. Add additional parts to the
 | 
|
37  | 
    message with add_inline_attachment(). Retrieve the entire formatted message
 | 
|
38  | 
    with as_string().
 | 
|
39  | 
||
40  | 
    Headers can be accessed with get() and msg[], and modified with msg[] =.
 | 
|
41  | 
    """
 | 
|
42  | 
||
43  | 
def __init__(self, from_address, to_address, subject, body=None):  | 
|
44  | 
"""Create an email message.  | 
|
45  | 
||
46  | 
        :param from_address: The origin address, to be put on the From header.
 | 
|
47  | 
        :param to_address: The destination address of the message, to be put in
 | 
|
48  | 
            the To header. Can also be a list of addresses.
 | 
|
49  | 
        :param subject: The subject of the message.
 | 
|
50  | 
        :param body: If given, the body of the message.
 | 
|
51  | 
||
52  | 
        All four parameters can be unicode strings or byte strings, but for the
 | 
|
53  | 
        addresses and subject byte strings must be encoded in UTF-8. For the
 | 
|
54  | 
        body any byte string will be accepted; if it's not ASCII or UTF-8,
 | 
|
55  | 
        it'll be sent with charset=8-bit.
 | 
|
56  | 
        """
 | 
|
57  | 
self._headers = {}  | 
|
58  | 
self._body = body  | 
|
59  | 
self._parts = []  | 
|
60  | 
||
61  | 
if isinstance(to_address, basestring):  | 
|
62  | 
to_address = [ to_address ]  | 
|
63  | 
||
64  | 
to_addresses = []  | 
|
65  | 
||
66  | 
for addr in to_address:  | 
|
67  | 
to_addresses.append(self.address_to_encoded_header(addr))  | 
|
68  | 
||
69  | 
self._headers['To'] = ', '.join(to_addresses)  | 
|
70  | 
self._headers['From'] = self.address_to_encoded_header(from_address)  | 
|
71  | 
self._headers['Subject'] = Header.Header(safe_unicode(subject))  | 
|
72  | 
self._headers['User-Agent'] = 'Bazaar (%s)' % _bzrlib_version  | 
|
73  | 
||
74  | 
def add_inline_attachment(self, body, filename=None, mime_subtype='plain'):  | 
|
75  | 
"""Add an inline attachment to the message.  | 
|
76  | 
||
77  | 
        :param body: A text to attach. Can be an unicode string or a byte
 | 
|
78  | 
            string, and it'll be sent as ascii, utf-8, or 8-bit, in that
 | 
|
79  | 
            preferred order.
 | 
|
80  | 
        :param filename: The name for the attachment. This will give a default
 | 
|
81  | 
            name for email programs to save the attachment.
 | 
|
82  | 
        :param mime_subtype: MIME subtype of the attachment (eg. 'plain' for
 | 
|
83  | 
            text/plain [default]).
 | 
|
84  | 
||
85  | 
        The attachment body will be displayed inline, so do not use this
 | 
|
86  | 
        function to attach binary attachments.
 | 
|
87  | 
        """
 | 
|
88  | 
        # add_inline_attachment() has been called, so the message will be a
 | 
|
89  | 
        # MIMEMultipart; add the provided body, if any, as the first attachment
 | 
|
90  | 
if self._body is not None:  | 
|
91  | 
self._parts.append((self._body, None, 'plain'))  | 
|
92  | 
self._body = None  | 
|
93  | 
||
94  | 
self._parts.append((body, filename, mime_subtype))  | 
|
95  | 
||
96  | 
def as_string(self, boundary=None):  | 
|
97  | 
"""Return the entire formatted message as a string.  | 
|
| 
3943.8.1
by Marius Kruger
 remove all trailing whitespace from bzr source  | 
98  | 
|
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
99  | 
        :param boundary: The boundary to use between MIME parts, if applicable.
 | 
100  | 
            Used for tests.
 | 
|
101  | 
        """
 | 
|
102  | 
if not self._parts:  | 
|
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
103  | 
msgobj = Message.Message()  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
104  | 
if self._body is not None:  | 
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
105  | 
body, encoding = self.string_with_encoding(self._body)  | 
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
106  | 
msgobj.set_payload(body, encoding)  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
107  | 
else:  | 
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
108  | 
msgobj = MIMEMultipart.MIMEMultipart()  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
109  | 
|
110  | 
if boundary is not None:  | 
|
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
111  | 
msgobj.set_boundary(boundary)  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
112  | 
|
113  | 
for body, filename, mime_subtype in self._parts:  | 
|
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
114  | 
body, encoding = self.string_with_encoding(body)  | 
115  | 
payload = MIMEText.MIMEText(body, mime_subtype, encoding)  | 
|
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
116  | 
|
117  | 
if filename is not None:  | 
|
118  | 
content_type = payload['Content-Type']  | 
|
119  | 
content_type += '; name="%s"' % filename  | 
|
120  | 
payload.replace_header('Content-Type', content_type)  | 
|
121  | 
||
122  | 
payload['Content-Disposition'] = 'inline'  | 
|
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
123  | 
msgobj.attach(payload)  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
124  | 
|
125  | 
        # sort headers here to ease testing
 | 
|
126  | 
for header, value in sorted(self._headers.items()):  | 
|
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
127  | 
msgobj[header] = value  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
128  | 
|
| 
2639.1.2
by John Arbash Meinel
 Some cleanups for the EmailMessage class.  | 
129  | 
return msgobj.as_string()  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
130  | 
|
131  | 
__str__ = as_string  | 
|
132  | 
||
133  | 
def get(self, header, failobj=None):  | 
|
134  | 
"""Get a header from the message, returning failobj if not present."""  | 
|
135  | 
return self._headers.get(header, failobj)  | 
|
136  | 
||
137  | 
def __getitem__(self, header):  | 
|
138  | 
"""Get a header from the message, returning None if not present.  | 
|
| 
3943.8.1
by Marius Kruger
 remove all trailing whitespace from bzr source  | 
139  | 
|
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
140  | 
        This method intentionally does not raise KeyError to mimic the behavior
 | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
141  | 
        of __getitem__ in email.Message.
 | 
142  | 
        """
 | 
|
143  | 
return self._headers.get(header, None)  | 
|
144  | 
||
145  | 
def __setitem__(self, header, value):  | 
|
146  | 
return self._headers.__setitem__(header, value)  | 
|
147  | 
||
148  | 
    @staticmethod
 | 
|
149  | 
def send(config, from_address, to_address, subject, body, attachment=None,  | 
|
150  | 
attachment_filename=None, attachment_mime_subtype='plain'):  | 
|
151  | 
"""Create an email message and send it with SMTPConnection.  | 
|
152  | 
||
153  | 
        :param config: config object to pass to SMTPConnection constructor.
 | 
|
154  | 
||
155  | 
        See EmailMessage.__init__() and EmailMessage.add_inline_attachment()
 | 
|
156  | 
        for an explanation of the rest of parameters.
 | 
|
157  | 
        """
 | 
|
158  | 
msg = EmailMessage(from_address, to_address, subject, body)  | 
|
159  | 
if attachment is not None:  | 
|
160  | 
msg.add_inline_attachment(attachment, attachment_filename,  | 
|
161  | 
attachment_mime_subtype)  | 
|
162  | 
SMTPConnection(config).send_email(msg)  | 
|
163  | 
||
164  | 
    @staticmethod
 | 
|
165  | 
def address_to_encoded_header(address):  | 
|
166  | 
"""RFC2047-encode an address if necessary.  | 
|
167  | 
||
168  | 
        :param address: An unicode string, or UTF-8 byte string.
 | 
|
169  | 
        :return: A possibly RFC2047-encoded string.
 | 
|
170  | 
        """
 | 
|
171  | 
        # Can't call Header over all the address, because that encodes both the
 | 
|
172  | 
        # name and the email address, which is not permitted by RFCs.
 | 
|
173  | 
user, email = Utils.parseaddr(address)  | 
|
174  | 
if not user:  | 
|
175  | 
return email  | 
|
176  | 
else:  | 
|
177  | 
return Utils.formataddr((str(Header.Header(safe_unicode(user))),  | 
|
178  | 
email))  | 
|
179  | 
||
180  | 
    @staticmethod
 | 
|
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
181  | 
def string_with_encoding(string_):  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
182  | 
"""Return a str object together with an encoding.  | 
183  | 
||
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
184  | 
        :param string_: A str or unicode object.
 | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
185  | 
        :return: A tuple (str, encoding), where encoding is one of 'ascii',
 | 
186  | 
            'utf-8', or '8-bit', in that preferred order.
 | 
|
187  | 
        """
 | 
|
188  | 
        # Python's email module base64-encodes the body whenever the charset is
 | 
|
189  | 
        # not explicitly set to ascii. Because of this, and because we want to
 | 
|
190  | 
        # avoid base64 when it's not necessary in order to be most compatible
 | 
|
191  | 
        # with the capabilities of the receiving side, we check with encode()
 | 
|
192  | 
        # and decode() whether the body is actually ascii-only.
 | 
|
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
193  | 
if isinstance(string_, unicode):  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
194  | 
try:  | 
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
195  | 
return (string_.encode('ascii'), 'ascii')  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
196  | 
except UnicodeEncodeError:  | 
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
197  | 
return (string_.encode('utf-8'), 'utf-8')  | 
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
198  | 
else:  | 
199  | 
try:  | 
|
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
200  | 
string_.decode('ascii')  | 
201  | 
return (string_, 'ascii')  | 
|
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
202  | 
except UnicodeDecodeError:  | 
203  | 
try:  | 
|
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
204  | 
string_.decode('utf-8')  | 
205  | 
return (string_, 'utf-8')  | 
|
| 
2625.6.1
by Adeodato Simó
 New EmailMessage class, façade around email.Message and MIMEMultipart.  | 
206  | 
except UnicodeDecodeError:  | 
| 
2625.6.3
by Adeodato Simó
 Changes after review by John.  | 
207  | 
return (string_, '8-bit')  |