~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
186
class Thunderbird(ExternalMailClient):
2681.1.11 by Aaron Bentley
Add docstrings, add compose_merge_request
187
    """Mozilla Thunderbird (or Icedove)
188
189
    Note that Thunderbird 1.5 is buggy and does not support setting
190
    "to" simultaneously with including a attachment.
191
192
    There is a workaround if no attachment is present, but we always need to
193
    send attachments.
194
    """
195
2681.1.37 by Aaron Bentley
Update docstrings and string formatting
196
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
197
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
198
199
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
200
        """See ExternalMailClient._get_compose_commandline"""
2681.1.8 by Aaron Bentley
Add Thunderbird support to bzr send
201
        message_options = {}
202
        if to is not None:
203
            message_options['to'] = to
204
        if subject is not None:
205
            message_options['subject'] = subject
206
        if attach_path is not None:
207
            message_options['attachment'] = urlutils.local_path_to_url(
208
                attach_path)
209
        options_list = ["%s='%s'" % (k, v) for k, v in
210
                        sorted(message_options.iteritems())]
211
        return ['-compose', ','.join(options_list)]
2681.1.23 by Aaron Bentley
Add support for xdg-email
212
213
2681.5.3 by ghigo
Add KMail mail client
214
class KMail(ExternalMailClient):
2681.5.1 by ghigo
Add KMail support to bzr send
215
    """KDE mail client."""
216
217
    _client_commands = ['kmail']
218
219
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
220
        """See ExternalMailClient._get_compose_commandline"""
2681.5.1 by ghigo
Add KMail support to bzr send
221
        message_options = []
222
        if subject is not None:
223
            message_options.extend( ['-s', subject ] )
224
        if attach_path is not None:
225
            message_options.extend( ['--attach', attach_path] )
226
        if to is not None:
227
            message_options.extend( [ to ] )
228
229
        return message_options
230
231
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
232
class XDGEmail(ExternalMailClient):
2681.1.23 by Aaron Bentley
Add support for xdg-email
233
    """xdg-email attempts to invoke the user's preferred mail client"""
234
235
    _client_commands = ['xdg-email']
236
237
    def _get_compose_commandline(self, to, subject, attach_path):
2681.1.36 by Aaron Bentley
Update docs
238
        """See ExternalMailClient._get_compose_commandline"""
2681.1.23 by Aaron Bentley
Add support for xdg-email
239
        commandline = [to]
240
        if subject is not None:
241
            commandline.extend(['--subject', subject])
242
        if attach_path is not None:
243
            commandline.extend(['--attach', attach_path])
244
        return commandline
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
245
246
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
247
class MAPIClient(ExternalMailClient):
248
    """Default Windows mail client launched using MAPI."""
249
250
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
251
                 extension):
2681.1.36 by Aaron Bentley
Update docs
252
        """See ExternalMailClient._compose.
253
254
        This implementation uses MAPI via the simplemapi ctypes wrapper
255
        """
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
256
        from bzrlib.util import simplemapi
257
        try:
258
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
2681.3.6 by Lukáš Lalinsky
New version of simplemapi.py with MIT license.
259
        except simplemapi.MAPIError, e:
260
            if e.code != simplemapi.MAPI_USER_ABORT:
261
                raise errors.MailClientNotFound(['MAPI supported mail client'
262
                                                 ' (error %d)' % (e.code,)])
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
263
264
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
265
class DefaultMail(MailClient):
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
266
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
267
    falls back to Editor"""
268
269
    def _mail_client(self):
2681.1.36 by Aaron Bentley
Update docs
270
        """Determine the preferred mail client for this platform"""
2681.3.4 by Lukáš Lalinsky
- Rename 'windows' to 'mapi'
271
        if osutils.supports_mapi():
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
272
            return MAPIClient(self.config)
273
        else:
274
            return XDGEmail(self.config)
2681.1.25 by Aaron Bentley
Cleanup
275
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
276
    def compose(self, prompt, to, subject, attachment, mime_subtype,
277
                extension):
2681.1.36 by Aaron Bentley
Update docs
278
        """See MailClient.compose"""
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
279
        try:
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
280
            return self._mail_client().compose(prompt, to, subject,
281
                                               attachment, mimie_subtype,
282
                                               extension)
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
283
        except errors.MailClientNotFound:
284
            return Editor(self.config).compose(prompt, to, subject,
285
                          attachment, mimie_subtype, extension)
286
287
    def compose_merge_request(self, to, subject, directive):
2681.1.36 by Aaron Bentley
Update docs
288
        """See MailClient.compose_merge_request"""
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
289
        try:
2681.3.1 by Lukáš Lalinsky
Support for sending bundles using MAPI on Windows.
290
            return self._mail_client().compose_merge_request(to, subject,
291
                                                             directive)
2681.1.24 by Aaron Bentley
Handle default mail client by trying xdg-email, falling back to editor
292
        except errors.MailClientNotFound:
293
            return Editor(self.config).compose_merge_request(to, subject,
294
                          directive)