1
# Copyright (C) 2007 Canonical Ltd
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.
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.
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
17
"""A convenience class around email.Message and email.MIMEMultipart."""
27
from bzrlib import __version__ as _bzrlib_version
28
from bzrlib.osutils import safe_unicode
29
from bzrlib.smtp_connection import SMTPConnection
32
class EmailMessage(object):
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
40
Headers can be accessed with get() and msg[], and modified with msg[] =.
43
def __init__(self, from_address, to_address, subject, body=None):
44
"""Create an email message.
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.
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.
62
if isinstance(to_address, basestring):
63
to_address = [ to_address ]
67
for addr in to_address:
68
to_addresses.append(self.address_to_encoded_header(addr))
70
self._headers['To'] = ', '.join(to_addresses)
71
self._headers['From'] = self.address_to_encoded_header(from_address)
72
self._headers['Subject'] = Header.Header(safe_unicode(subject))
73
self._headers['User-Agent'] = 'Bazaar (%s)' % _bzrlib_version
75
def add_inline_attachment(self, body, filename=None, mime_subtype='plain'):
76
"""Add an inline attachment to the message.
78
:param body: A text to attach. Can be an unicode string or a byte
79
string, and it'll be sent as ascii, utf-8, or 8-bit, in that
81
:param filename: The name for the attachment. This will give a default
82
name for email programs to save the attachment.
83
:param mime_subtype: MIME subtype of the attachment (eg. 'plain' for
84
text/plain [default]).
86
The attachment body will be displayed inline, so do not use this
87
function to attach binary attachments.
89
# add_inline_attachment() has been called, so the message will be a
90
# MIMEMultipart; add the provided body, if any, as the first attachment
91
if self._body is not None:
92
self._parts.append((self._body, None, 'plain'))
95
self._parts.append((body, filename, mime_subtype))
98
def as_string(self, boundary=None):
99
"""Return the entire formatted message as a string.
101
:param boundary: The boundary to use between MIME parts, if applicable.
104
if self._msgobj is not None:
105
return self._msgobj.as_string()
108
self._msgobj = Message.Message()
109
if self._body is not None:
110
body, encoding = self.string_with_encoding(self._body)
111
self._msgobj.set_payload(body, encoding)
113
self._msgobj = MIMEMultipart.MIMEMultipart()
115
if boundary is not None:
116
self._msgobj.set_boundary(boundary)
118
for body, filename, mime_subtype in self._parts:
119
body, encoding = self.string_with_encoding(body)
120
payload = MIMEText.MIMEText(body, mime_subtype, encoding)
122
if filename is not None:
123
content_type = payload['Content-Type']
124
content_type += '; name="%s"' % filename
125
payload.replace_header('Content-Type', content_type)
127
payload['Content-Disposition'] = 'inline'
128
self._msgobj.attach(payload)
130
# sort headers here to ease testing
131
for header, value in sorted(self._headers.items()):
132
self._msgobj[header] = value
134
return self._msgobj.as_string()
138
def get(self, header, failobj=None):
139
"""Get a header from the message, returning failobj if not present."""
140
return self._headers.get(header, failobj)
142
def __getitem__(self, header):
143
"""Get a header from the message, returning None if not present.
145
This method intentionally does not raise KeyError to mimic the behavior
146
of __getitem__ in email.Message.
148
return self._headers.get(header, None)
150
def __setitem__(self, header, value):
151
return self._headers.__setitem__(header, value)
154
def send(config, from_address, to_address, subject, body, attachment=None,
155
attachment_filename=None, attachment_mime_subtype='plain'):
156
"""Create an email message and send it with SMTPConnection.
158
:param config: config object to pass to SMTPConnection constructor.
160
See EmailMessage.__init__() and EmailMessage.add_inline_attachment()
161
for an explanation of the rest of parameters.
163
msg = EmailMessage(from_address, to_address, subject, body)
164
if attachment is not None:
165
msg.add_inline_attachment(attachment, attachment_filename,
166
attachment_mime_subtype)
167
SMTPConnection(config).send_email(msg)
170
def address_to_encoded_header(address):
171
"""RFC2047-encode an address if necessary.
173
:param address: An unicode string, or UTF-8 byte string.
174
:return: A possibly RFC2047-encoded string.
176
# Can't call Header over all the address, because that encodes both the
177
# name and the email address, which is not permitted by RFCs.
178
user, email = Utils.parseaddr(address)
182
return Utils.formataddr((str(Header.Header(safe_unicode(user))),
186
def string_with_encoding(string_):
187
"""Return a str object together with an encoding.
189
:param string_: A str or unicode object.
190
:return: A tuple (str, encoding), where encoding is one of 'ascii',
191
'utf-8', or '8-bit', in that preferred order.
193
# Python's email module base64-encodes the body whenever the charset is
194
# not explicitly set to ascii. Because of this, and because we want to
195
# avoid base64 when it's not necessary in order to be most compatible
196
# with the capabilities of the receiving side, we check with encode()
197
# and decode() whether the body is actually ascii-only.
198
if isinstance(string_, unicode):
200
return (string_.encode('ascii'), 'ascii')
201
except UnicodeEncodeError:
202
return (string_.encode('utf-8'), 'utf-8')
205
string_.decode('ascii')
206
return (string_, 'ascii')
207
except UnicodeDecodeError:
209
string_.decode('utf-8')
210
return (string_, 'utf-8')
211
except UnicodeDecodeError:
212
return (string_, '8-bit')