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 .osutils import safe_unicode
40
from .smtp_connection import SMTPConnection
43
class EmailMessage(object):
46
The constructor needs an origin address, a destination address or addresses
47
and a subject, and accepts a body as well. Add additional parts to the
48
message with add_inline_attachment(). Retrieve the entire formatted message
51
Headers can be accessed with get() and msg[], and modified with msg[] =.
54
def __init__(self, from_address, to_address, subject, body=None):
55
"""Create an email message.
57
:param from_address: The origin address, to be put on the From header.
58
:param to_address: The destination address of the message, to be put in
59
the To header. Can also be a list of addresses.
60
:param subject: The subject of the message.
61
:param body: If given, the body of the message.
63
All four parameters can be unicode strings or byte strings, but for the
64
addresses and subject byte strings must be encoded in UTF-8. For the
65
body any byte string will be accepted; if it's not ASCII or UTF-8,
66
it'll be sent with charset=8-bit.
72
if isinstance(to_address, (str, text_type)):
73
to_address = [ to_address ]
77
for addr in to_address:
78
to_addresses.append(self.address_to_encoded_header(addr))
80
self._headers['To'] = ', '.join(to_addresses)
81
self._headers['From'] = self.address_to_encoded_header(from_address)
82
self._headers['Subject'] = Header(safe_unicode(subject))
83
self._headers['User-Agent'] = 'Bazaar (%s)' % _breezy_version
85
def add_inline_attachment(self, body, filename=None, mime_subtype='plain'):
86
"""Add an inline attachment to the message.
88
:param body: A text to attach. Can be an unicode string or a byte
89
string, and it'll be sent as ascii, utf-8, or 8-bit, in that
91
:param filename: The name for the attachment. This will give a default
92
name for email programs to save the attachment.
93
:param mime_subtype: MIME subtype of the attachment (eg. 'plain' for
94
text/plain [default]).
96
The attachment body will be displayed inline, so do not use this
97
function to attach binary attachments.
99
# add_inline_attachment() has been called, so the message will be a
100
# MIMEMultipart; add the provided body, if any, as the first attachment
101
if self._body is not None:
102
self._parts.append((self._body, None, 'plain'))
105
self._parts.append((body, filename, mime_subtype))
107
def as_string(self, boundary=None):
108
"""Return the entire formatted message as a string.
110
:param boundary: The boundary to use between MIME parts, if applicable.
115
if self._body is not None:
116
body, encoding = self.string_with_encoding(self._body)
117
msgobj.set_payload(body, encoding)
119
msgobj = MIMEMultipart()
121
if boundary is not None:
122
msgobj.set_boundary(boundary)
124
for body, filename, mime_subtype in self._parts:
125
body, encoding = self.string_with_encoding(body)
126
payload = MIMEText(body, mime_subtype, encoding)
128
if filename is not None:
129
content_type = payload['Content-Type']
130
content_type += '; name="%s"' % filename
131
payload.replace_header('Content-Type', content_type)
133
payload['Content-Disposition'] = 'inline'
134
msgobj.attach(payload)
136
# sort headers here to ease testing
137
for header, value in sorted(self._headers.items()):
138
msgobj[header] = value
140
return msgobj.as_string()
144
def get(self, header, failobj=None):
145
"""Get a header from the message, returning failobj if not present."""
146
return self._headers.get(header, failobj)
148
def __getitem__(self, header):
149
"""Get a header from the message, returning None if not present.
151
This method intentionally does not raise KeyError to mimic the behavior
152
of __getitem__ in email.Message.
154
return self._headers.get(header, None)
156
def __setitem__(self, header, value):
157
return self._headers.__setitem__(header, value)
160
def send(config, from_address, to_address, subject, body, attachment=None,
161
attachment_filename=None, attachment_mime_subtype='plain'):
162
"""Create an email message and send it with SMTPConnection.
164
:param config: config object to pass to SMTPConnection constructor.
166
See EmailMessage.__init__() and EmailMessage.add_inline_attachment()
167
for an explanation of the rest of parameters.
169
msg = EmailMessage(from_address, to_address, subject, body)
170
if attachment is not None:
171
msg.add_inline_attachment(attachment, attachment_filename,
172
attachment_mime_subtype)
173
SMTPConnection(config).send_email(msg)
176
def address_to_encoded_header(address):
177
"""RFC2047-encode an address if necessary.
179
:param address: An unicode string, or UTF-8 byte string.
180
:return: A possibly RFC2047-encoded string.
182
# Can't call Header over all the address, because that encodes both the
183
# name and the email address, which is not permitted by RFCs.
184
user, email = parseaddr(address)
188
return formataddr((str(Header(safe_unicode(user))),
192
def string_with_encoding(string_):
193
"""Return a str object together with an encoding.
195
:param string\\_: A str or unicode object.
196
:return: A tuple (str, encoding), where encoding is one of 'ascii',
197
'utf-8', or '8-bit', in that preferred order.
199
# Python's email module base64-encodes the body whenever the charset is
200
# not explicitly set to ascii. Because of this, and because we want to
201
# avoid base64 when it's not necessary in order to be most compatible
202
# with the capabilities of the receiving side, we check with encode()
203
# and decode() whether the body is actually ascii-only.
204
if isinstance(string_, unicode):
206
return (string_.encode('ascii'), 'ascii')
207
except UnicodeEncodeError:
208
return (string_.encode('utf-8'), 'utf-8')
211
string_.decode('ascii')
212
return (string_, 'ascii')
213
except UnicodeDecodeError:
215
string_.decode('utf-8')
216
return (string_, 'utf-8')
217
except UnicodeDecodeError:
218
return (string_, '8-bit')