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."""
 
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.
 
61
 
        if isinstance(to_address, basestring):
 
62
 
            to_address = [ to_address ]
 
66
 
        for addr in to_address:
 
67
 
            to_addresses.append(self.address_to_encoded_header(addr))
 
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
 
74
 
    def add_inline_attachment(self, body, filename=None, mime_subtype='plain'):
 
75
 
        """Add an inline attachment to the message.
 
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
 
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]).
 
85
 
        The attachment body will be displayed inline, so do not use this
 
86
 
        function to attach binary attachments.
 
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'))
 
94
 
        self._parts.append((body, filename, mime_subtype))
 
96
 
    def as_string(self, boundary=None):
 
97
 
        """Return the entire formatted message as a string.
 
99
 
        :param boundary: The boundary to use between MIME parts, if applicable.
 
103
 
            msgobj = Message.Message()
 
104
 
            if self._body is not None:
 
105
 
                body, encoding = self.string_with_encoding(self._body)
 
106
 
                msgobj.set_payload(body, encoding)
 
108
 
            msgobj = MIMEMultipart.MIMEMultipart()
 
110
 
            if boundary is not None:
 
111
 
                msgobj.set_boundary(boundary)
 
113
 
            for body, filename, mime_subtype in self._parts:
 
114
 
                body, encoding = self.string_with_encoding(body)
 
115
 
                payload = MIMEText.MIMEText(body, mime_subtype, encoding)
 
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)
 
122
 
                payload['Content-Disposition'] = 'inline'
 
123
 
                msgobj.attach(payload)
 
125
 
        # sort headers here to ease testing
 
126
 
        for header, value in sorted(self._headers.items()):
 
127
 
            msgobj[header] = value
 
129
 
        return msgobj.as_string()
 
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)
 
137
 
    def __getitem__(self, header):
 
138
 
        """Get a header from the message, returning None if not present.
 
140
 
        This method intentionally does not raise KeyError to mimic the behavior
 
141
 
        of __getitem__ in email.Message.
 
143
 
        return self._headers.get(header, None)
 
145
 
    def __setitem__(self, header, value):
 
146
 
        return self._headers.__setitem__(header, value)
 
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.
 
153
 
        :param config: config object to pass to SMTPConnection constructor.
 
155
 
        See EmailMessage.__init__() and EmailMessage.add_inline_attachment()
 
156
 
        for an explanation of the rest of parameters.
 
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)
 
165
 
    def address_to_encoded_header(address):
 
166
 
        """RFC2047-encode an address if necessary.
 
168
 
        :param address: An unicode string, or UTF-8 byte string.
 
169
 
        :return: A possibly RFC2047-encoded string.
 
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)
 
177
 
            return Utils.formataddr((str(Header.Header(safe_unicode(user))),
 
181
 
    def string_with_encoding(string_):
 
182
 
        """Return a str object together with an encoding.
 
184
 
        :param string_: A str or unicode object.
 
185
 
        :return: A tuple (str, encoding), where encoding is one of 'ascii',
 
186
 
            'utf-8', or '8-bit', in that preferred order.
 
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.
 
193
 
        if isinstance(string_, unicode):
 
195
 
                return (string_.encode('ascii'), 'ascii')
 
196
 
            except UnicodeEncodeError:
 
197
 
                return (string_.encode('utf-8'), 'utf-8')
 
200
 
                string_.decode('ascii')
 
201
 
                return (string_, 'ascii')
 
202
 
            except UnicodeDecodeError:
 
204
 
                    string_.decode('utf-8')
 
205
 
                    return (string_, 'utf-8')
 
206
 
                except UnicodeDecodeError:
 
207
 
                    return (string_, '8-bit')