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