~bzr-pqm/bzr/bzr.dev

2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
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
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
17
import errno
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
18
import os
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
19
import subprocess
2681.4.1 by Alexander Belchenko
win32: looking for full path of mail client executable in registry
20
import sys
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
21
import tempfile
22
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
23
import bzrlib
2681.1.9 by Aaron Bentley
Add support for mail-from-editor
24
from bzrlib import (
25
    email_message,
26
    errors,
27
    msgeditor,
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
28
    osutils,
2681.1.9 by Aaron Bentley
Add support for mail-from-editor
29
    urlutils,
30
    )
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
31
32
33
class MailClient(object):
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
34
    """A mail client that can send messages with attachements."""
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
35
2681.1.9 by Aaron Bentley
Add support for mail-from-editor
36
    def __init__(self, config):
37
        self.config = config
38
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
39
    def compose(self, prompt, to, subject, attachment, mime_subtype,
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
40
                extension, basename=None):
2681.1.36 by Aaron Bentley
Update docs
41
        """Compose (and possibly send) an email message
42
43
        Must be implemented by subclasses.
44
45
        :param prompt: A message to tell the user what to do.  Supported by
46
            the Editor client, but ignored by others
47
        :param to: The address to send the message to
48
        :param subject: The contents of the subject line
49
        :param attachment: An email attachment, as a bytestring
50
        :param mime_subtype: The attachment is assumed to be a subtype of
51
            Text.  This allows the precise subtype to be specified, e.g.
52
            "plain", "x-patch", etc.
53
        :param extension: The file extension associated with the attachment
54
            type, e.g. ".patch"
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
55
        :param basename: The name to use for the attachment, e.g.
56
            "send-nick-3252"
2681.1.36 by Aaron Bentley
Update docs
57
        """
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
58
        raise NotImplementedError
59
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
60
    def compose_merge_request(self, to, subject, directive, basename=None):
2681.1.36 by Aaron Bentley
Update docs
61
        """Compose (and possibly send) a merge request
62
63
        :param to: The address to send the request to
64
        :param subject: The subject line to use for the request
65
        :param directive: A merge directive representing the merge request, as
66
            a bytestring.
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
67
        :param basename: The name to use for the attachment, e.g.
68
            "send-nick-3252"
2681.1.36 by Aaron Bentley
Update docs
69
        """
2681.1.21 by Aaron Bentley
Refactor prompt generation to make it testable, test it with unicode
70
        prompt = self._get_merge_prompt("Please describe these changes:", to,
71
                                        subject, directive)
72
        self.compose(prompt, to, subject, directive,
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
73
            'x-patch', '.patch', basename)
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
74
2681.1.21 by Aaron Bentley
Refactor prompt generation to make it testable, test it with unicode
75
    def _get_merge_prompt(self, prompt, to, subject, attachment):
2681.1.36 by Aaron Bentley
Update docs
76
        """Generate a prompt string.  Overridden by Editor.
77
78
        :param prompt: A string suggesting what user should do
79
        :param to: The address the mail will be sent to
80
        :param subject: The subject line of the mail
81
        :param attachment: The attachment that will be used
82
        """
2681.1.21 by Aaron Bentley
Refactor prompt generation to make it testable, test it with unicode
83
        return ''
84
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
85
86
class Editor(MailClient):
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
87
    """DIY mail client that uses commit message editor"""
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
88
2681.1.21 by Aaron Bentley
Refactor prompt generation to make it testable, test it with unicode
89
    def _get_merge_prompt(self, prompt, to, subject, attachment):
2681.1.37 by Aaron Bentley
Update docstrings and string formatting
90
        """See MailClient._get_merge_prompt"""
91
        return (u"%s\n\n"
92
                u"To: %s\n"
93
                u"Subject: %s\n\n"
94
                u"%s" % (prompt, to, subject,
95
                         attachment.decode('utf-8', 'replace')))
2681.1.21 by Aaron Bentley
Refactor prompt generation to make it testable, test it with unicode
96
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
97
    def compose(self, prompt, to, subject, attachment, mime_subtype,
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
98
                extension, basename=None):
2681.1.37 by Aaron Bentley
Update docstrings and string formatting
99
        """See MailClient.compose"""
3042.1.1 by Lukáš Lalinský
Make mail-to address in ``bzr send`` optional for interactive mail clients.
100
        if not to:
101
            raise errors.NoMailAddressSpecified()
2681.1.21 by Aaron Bentley
Refactor prompt generation to make it testable, test it with unicode
102
        body = msgeditor.edit_commit_message(prompt)
2681.1.9 by Aaron Bentley
Add support for mail-from-editor
103
        if body == '':
104
            raise errors.NoMessageSupplied()
105
        email_message.EmailMessage.send(self.config,
106
                                        self.config.username(),
107
                                        to,
108
                                        subject,
109
                                        body,
110
                                        attachment,
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
111
                                        attachment_mime_subtype=mime_subtype)
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
112
113
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
114
class ExternalMailClient(MailClient):
115
    """An external mail client."""
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
116
2681.4.1 by Alexander Belchenko
win32: looking for full path of mail client executable in registry
117
    def _get_client_commands(self):
2681.1.36 by Aaron Bentley
Update docs
118
        """Provide a list of commands that may invoke the mail client"""
2681.4.1 by Alexander Belchenko
win32: looking for full path of mail client executable in registry
119
        if sys.platform == 'win32':
2681.1.29 by Aaron Bentley
Make conditional import explicit
120
            import win32utils
2681.4.1 by Alexander Belchenko
win32: looking for full path of mail client executable in registry
121
            return [win32utils.get_app_path(i) for i in self._client_commands]
122
        else:
123
            return self._client_commands
124
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
125
    def compose(self, prompt, to, subject, attachment, mime_subtype,
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
126
                extension, basename=None):
2681.1.36 by Aaron Bentley
Update docs
127
        """See MailClient.compose.
128
129
        Writes the attachment to a temporary file, invokes _compose.
130
        """
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
131
        if basename is None:
132
            basename = 'attachment'
133
        pathname = tempfile.mkdtemp(prefix='bzr-mail-')
134
        attach_path = osutils.pathjoin(pathname, basename + extension)
135
        outfile = open(attach_path, 'wb')
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
136
        try:
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
137
            outfile.write(attachment)
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
138
        finally:
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
139
            outfile.close()
140
        self._compose(prompt, to, subject, attach_path, mime_subtype,
141
                      extension)
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
142
143
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
144
                extension):
2681.1.36 by Aaron Bentley
Update docs
145
        """Invoke a mail client as a commandline process.
146
147
        Overridden by MAPIClient.
148
        :param to: The address to send the mail to
149
        :param subject: The subject line for the mail
150
        :param pathname: The path to the attachment
151
        :param mime_subtype: The attachment is assumed to have a major type of
152
            "text", but the precise subtype can be specified here
153
        :param extension: A file extension (including period) associated with
154
            the attachment type.
155
        """
2681.4.1 by Alexander Belchenko
win32: looking for full path of mail client executable in registry
156
        for name in self._get_client_commands():
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
157
            cmdline = [self._encode_path(name, 'executable')]
158
            cmdline.extend(self._get_compose_commandline(to, subject,
159
                                                         attach_path))
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
160
            try:
161
                subprocess.call(cmdline)
162
            except OSError, e:
163
                if e.errno != errno.ENOENT:
164
                    raise
165
            else:
166
                break
167
        else:
168
            raise errors.MailClientNotFound(self._client_commands)
169
170
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
171
        """Determine the commandline to use for composing a message
172
173
        Implemented by various subclasses
174
        :param to: The address to send the mail to
175
        :param subject: The subject line for the mail
176
        :param attach_path: The path to the attachment
177
        """
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
178
        raise NotImplementedError
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
179
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
180
    def _encode_safe(self, u):
181
        """Encode possible unicode string argument to 8-bit string
182
        in user_encoding. Unencodable characters will be replaced
183
        with '?'.
184
185
        :param  u:  possible unicode string.
186
        :return:    encoded string if u is unicode, u itself otherwise.
187
        """
188
        if isinstance(u, unicode):
189
            return u.encode(bzrlib.user_encoding, 'replace')
190
        return u
191
192
    def _encode_path(self, path, kind):
193
        """Encode unicode path in user encoding.
194
195
        :param  path:   possible unicode path.
196
        :param  kind:   path kind ('executable' or 'attachment').
197
        :return:        encoded path if path is unicode,
198
                        path itself otherwise.
199
        :raise:         UnableEncodePath.
200
        """
201
        if isinstance(path, unicode):
202
            try:
203
                return path.encode(bzrlib.user_encoding)
204
            except UnicodeEncodeError:
205
                raise errors.UnableEncodePath(path, kind)
206
        return path
3234.2.3 by Alexander Belchenko
mail_client.py: provide new private method ExternalMailClient._get_compose_8bit_commandline to make bug #139318 testable (as Aaron requested).
207
2681.1.18 by Aaron Bentley
Refactor to increase code sharing, allow multiple command names for tbird
208
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
209
class Evolution(ExternalMailClient):
210
    """Evolution mail client."""
211
212
    _client_commands = ['evolution']
213
214
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
215
        """See ExternalMailClient._get_compose_commandline"""
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
216
        message_options = {}
217
        if subject is not None:
218
            message_options['subject'] = subject
219
        if attach_path is not None:
220
            message_options['attach'] = attach_path
221
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
222
                        sorted(message_options.iteritems())]
223
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
224
            '&'.join(options_list))]
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
225
226
2790.2.1 by Keir Mierle
Add Mutt as a supported client email program. Also rearranges various listings
227
class Mutt(ExternalMailClient):
228
    """Mutt mail client."""
229
230
    _client_commands = ['mutt']
231
232
    def _get_compose_commandline(self, to, subject, attach_path):
233
        """See ExternalMailClient._get_compose_commandline"""
234
        message_options = []
235
        if subject is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
236
            message_options.extend(['-s', self._encode_safe(subject)])
2790.2.1 by Keir Mierle
Add Mutt as a supported client email program. Also rearranges various listings
237
        if attach_path is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
238
            message_options.extend(['-a',
239
                self._encode_path(attach_path, 'attachment')])
2790.2.1 by Keir Mierle
Add Mutt as a supported client email program. Also rearranges various listings
240
        if to is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
241
            message_options.append(self._encode_safe(to))
2790.2.1 by Keir Mierle
Add Mutt as a supported client email program. Also rearranges various listings
242
        return message_options
243
244
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
245
class Thunderbird(ExternalMailClient):
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
246
    """Mozilla Thunderbird (or Icedove)
247
248
    Note that Thunderbird 1.5 is buggy and does not support setting
249
    "to" simultaneously with including a attachment.
250
251
    There is a workaround if no attachment is present, but we always need to
252
    send attachments.
253
    """
254
2681.1.37 by Aaron Bentley
Update docstrings and string formatting
255
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
256
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
257
258
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
259
        """See ExternalMailClient._get_compose_commandline"""
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
260
        message_options = {}
261
        if to is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
262
            message_options['to'] = self._encode_safe(to)
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
263
        if subject is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
264
            message_options['subject'] = self._encode_safe(subject)
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
265
        if attach_path is not None:
3234.2.2 by Alexander Belchenko
[merge] URL is always ascii.
266
            message_options['attachment'] = urlutils.local_path_to_url(
267
                attach_path)
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
268
        options_list = ["%s='%s'" % (k, v) for k, v in
269
                        sorted(message_options.iteritems())]
270
        return ['-compose', ','.join(options_list)]
2681.1.23 by Aaron Bentley
Add support for xdg-email
271
272
2681.5.3 by ghigo
Add KMail mail client
273
class KMail(ExternalMailClient):
2681.5.1 by ghigo
Add KMail support to bzr send
274
    """KDE mail client."""
275
276
    _client_commands = ['kmail']
277
278
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
279
        """See ExternalMailClient._get_compose_commandline"""
2681.5.1 by ghigo
Add KMail support to bzr send
280
        message_options = []
281
        if subject is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
282
            message_options.extend(['-s', self._encode_safe(subject)])
2681.5.1 by ghigo
Add KMail support to bzr send
283
        if attach_path is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
284
            message_options.extend(['--attach',
285
                self._encode_path(attach_path, 'attachment')])
2681.5.1 by ghigo
Add KMail support to bzr send
286
        if to is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
287
            message_options.extend([self._encode_safe(to)])
2681.5.1 by ghigo
Add KMail support to bzr send
288
        return message_options
289
290
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
291
class XDGEmail(ExternalMailClient):
2681.1.23 by Aaron Bentley
Add support for xdg-email
292
    """xdg-email attempts to invoke the user's preferred mail client"""
293
294
    _client_commands = ['xdg-email']
295
296
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
297
        """See ExternalMailClient._get_compose_commandline"""
3042.1.1 by Lukáš Lalinský
Make mail-to address in ``bzr send`` optional for interactive mail clients.
298
        if not to:
299
            raise errors.NoMailAddressSpecified()
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
300
        commandline = [self._encode_safe(to)]
2681.1.23 by Aaron Bentley
Add support for xdg-email
301
        if subject is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
302
            commandline.extend(['--subject', self._encode_safe(subject)])
2681.1.23 by Aaron Bentley
Add support for xdg-email
303
        if attach_path is not None:
3234.2.6 by Alexander Belchenko
because every mail client has different rules to compose command line we should encode arguments to 8 bit string only when needed.
304
            commandline.extend(['--attach',
305
                self._encode_path(attach_path, 'attachment')])
2681.1.23 by Aaron Bentley
Add support for xdg-email
306
        return commandline
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
307
308
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
309
class MAPIClient(ExternalMailClient):
310
    """Default Windows mail client launched using MAPI."""
311
312
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
313
                 extension):
2681.1.36 by Aaron Bentley
Update docs
314
        """See ExternalMailClient._compose.
315
316
        This implementation uses MAPI via the simplemapi ctypes wrapper
317
        """
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
318
        from bzrlib.util import simplemapi
319
        try:
320
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
2681.3.6 by Lukáš Lalinsky
New version of simplemapi.py with MIT license.
321
        except simplemapi.MAPIError, e:
322
            if e.code != simplemapi.MAPI_USER_ABORT:
323
                raise errors.MailClientNotFound(['MAPI supported mail client'
324
                                                 ' (error %d)' % (e.code,)])
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
325
326
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
327
class DefaultMail(MailClient):
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
328
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
329
    falls back to Editor"""
330
331
    def _mail_client(self):
2681.1.36 by Aaron Bentley
Update docs
332
        """Determine the preferred mail client for this platform"""
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
333
        if osutils.supports_mapi():
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
334
            return MAPIClient(self.config)
335
        else:
336
            return XDGEmail(self.config)
2681.1.25 by Aaron Bentley
Cleanup
337
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
338
    def compose(self, prompt, to, subject, attachment, mime_subtype,
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
339
                extension, basename=None):
2681.1.36 by Aaron Bentley
Update docs
340
        """See MailClient.compose"""
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
341
        try:
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
342
            return self._mail_client().compose(prompt, to, subject,
343
                                               attachment, mimie_subtype,
3251.2.1 by Aaron Bentley
Use nick/revno-based names for merge directives
344
                                               extension, basename)
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
345
        except errors.MailClientNotFound:
346
            return Editor(self.config).compose(prompt, to, subject,
347
                          attachment, mimie_subtype, extension)
348
349
    def compose_merge_request(self, to, subject, directive):
2681.1.36 by Aaron Bentley
Update docs
350
        """See MailClient.compose_merge_request"""
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
351
        try:
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
352
            return self._mail_client().compose_merge_request(to, subject,
353
                                                             directive)
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
354
        except errors.MailClientNotFound:
355
            return Editor(self.config).compose_merge_request(to, subject,
356
                          directive)