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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""A convenience class around email.Message and email.MIMEMultipart."""
19
from __future__ import absolute_import
22
from email.message import Message
23
from email.header import Header
24
from email.mime.multipart import MIMEMultipart
25
from email.mime.text import MIMEText
26
from email.utils import formataddr, parseaddr
27
except ImportError: # python < 3
34
from email.Utils import formataddr, parseaddr
35
from . import __version__ as _breezy_version
36
from .errors import BzrBadParameterNotUnicode
37
from .osutils import safe_unicode
41
from .smtp_connection import SMTPConnection
44
class EmailMessage(object):
47
The constructor needs an origin address, a destination address or addresses
48
and a subject, and accepts a body as well. Add additional parts to the
49
message with add_inline_attachment(). Retrieve the entire formatted message
52
Headers can be accessed with get() and msg[], and modified with msg[] =.
55
def __init__(self, from_address, to_address, subject, body=None):
56
"""Create an email message.
58
:param from_address: The origin address, to be put on the From header.
59
:param to_address: The destination address of the message, to be put in
60
the To header. Can also be a list of addresses.
61
:param subject: The subject of the message.
62
:param body: If given, the body of the message.
64
All four parameters can be unicode strings or byte strings, but for the
65
addresses and subject byte strings must be encoded in UTF-8. For the
66
body any byte string will be accepted; if it's not ASCII or UTF-8,
67
it'll be sent with charset=8-bit.
73
if isinstance(to_address, (bytes, text_type)):
74
to_address = [to_address]
78
for addr in to_address:
79
to_addresses.append(self.address_to_encoded_header(addr))
81
self._headers['To'] = ', '.join(to_addresses)
82
self._headers['From'] = self.address_to_encoded_header(from_address)
83
self._headers['Subject'] = Header(safe_unicode(subject))
84
self._headers['User-Agent'] = 'Bazaar (%s)' % _breezy_version
86
def add_inline_attachment(self, body, filename=None, mime_subtype='plain'):
87
"""Add an inline attachment to the message.
89
:param body: A text to attach. Can be an unicode string or a byte
90
string, and it'll be sent as ascii, utf-8, or 8-bit, in that
92
:param filename: The name for the attachment. This will give a default
93
name for email programs to save the attachment.
94
:param mime_subtype: MIME subtype of the attachment (eg. 'plain' for
95
text/plain [default]).
97
The attachment body will be displayed inline, so do not use this
98
function to attach binary attachments.
100
# add_inline_attachment() has been called, so the message will be a
101
# MIMEMultipart; add the provided body, if any, as the first attachment
102
if self._body is not None:
103
self._parts.append((self._body, None, 'plain'))
106
self._parts.append((body, filename, mime_subtype))
108
def as_string(self, boundary=None):
109
"""Return the entire formatted message as a string.
111
:param boundary: The boundary to use between MIME parts, if applicable.
116
if self._body is not None:
117
body, encoding = self.string_with_encoding(self._body)
118
msgobj.set_payload(body, encoding)
120
msgobj = MIMEMultipart()
122
if boundary is not None:
123
msgobj.set_boundary(boundary)
125
for body, filename, mime_subtype in self._parts:
126
body, encoding = self.string_with_encoding(body)
127
payload = MIMEText(body, mime_subtype, encoding)
129
if filename is not None:
130
content_type = payload['Content-Type']
131
content_type += '; name="%s"' % filename
132
payload.replace_header('Content-Type', content_type)
134
payload['Content-Disposition'] = 'inline'
135
msgobj.attach(payload)
137
# sort headers here to ease testing
138
for header, value in sorted(self._headers.items()):
139
msgobj[header] = value
141
return msgobj.as_string()
145
def get(self, header, failobj=None):
146
"""Get a header from the message, returning failobj if not present."""
147
return self._headers.get(header, failobj)
149
def __getitem__(self, header):
150
"""Get a header from the message, returning None if not present.
152
This method intentionally does not raise KeyError to mimic the behavior
153
of __getitem__ in email.Message.
155
return self._headers.get(header, None)
157
def __setitem__(self, header, value):
158
return self._headers.__setitem__(header, value)
161
def send(config, from_address, to_address, subject, body, attachment=None,
162
attachment_filename=None, attachment_mime_subtype='plain'):
163
"""Create an email message and send it with SMTPConnection.
165
:param config: config object to pass to SMTPConnection constructor.
167
See EmailMessage.__init__() and EmailMessage.add_inline_attachment()
168
for an explanation of the rest of parameters.
170
msg = EmailMessage(from_address, to_address, subject, body)
171
if attachment is not None:
172
msg.add_inline_attachment(attachment, attachment_filename,
173
attachment_mime_subtype)
174
SMTPConnection(config).send_email(msg)
177
def address_to_encoded_header(address):
178
"""RFC2047-encode an address if necessary.
180
:param address: An unicode string, or UTF-8 byte string.
181
:return: A possibly RFC2047-encoded string.
183
if not isinstance(address, (str, text_type)):
184
raise BzrBadParameterNotUnicode(address)
185
# Can't call Header over all the address, because that encodes both the
186
# name and the email address, which is not permitted by RFCs.
187
user, email = parseaddr(address)
191
return formataddr((str(Header(safe_unicode(user))),
195
def string_with_encoding(string_):
196
"""Return a str object together with an encoding.
198
:param string\\_: A str or unicode object.
199
:return: A tuple (str, encoding), where encoding is one of 'ascii',
200
'utf-8', or '8-bit', in that preferred order.
202
# Python's email module base64-encodes the body whenever the charset is
203
# not explicitly set to ascii. Because of this, and because we want to
204
# avoid base64 when it's not necessary in order to be most compatible
205
# with the capabilities of the receiving side, we check with encode()
206
# and decode() whether the body is actually ascii-only.
207
if isinstance(string_, text_type):
209
return (string_.encode('ascii'), 'ascii')
210
except UnicodeEncodeError:
211
return (string_.encode('utf-8'), 'utf-8')
214
string_.decode('ascii')
215
return (string_, 'ascii')
216
except UnicodeDecodeError:
218
string_.decode('utf-8')
219
return (string_, 'utf-8')
220
except UnicodeDecodeError:
221
return (string_, '8-bit')