/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/mail_client.py

  • Committer: Robert Collins
  • Date: 2007-08-26 22:10:51 UTC
  • mto: (2592.3.117 repository)
  • mto: This revision was merged to the branch mainline in revision 2885.
  • Revision ID: robertc@robertcollins.net-20070826221051-46uq33p3oqkscdd0
* New parameter on ``bzrlib.transport.Transport.readv``
  ``adjust_for_latency`` which changes readv from returning strictly the
  requested data to inserted return larger ranges and in forward read order
  to reduce the effect of network latency. (Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
17
import errno
 
18
import os
 
19
import subprocess
 
20
import sys
 
21
import tempfile
 
22
 
 
23
from bzrlib import (
 
24
    email_message,
 
25
    errors,
 
26
    msgeditor,
 
27
    osutils,
 
28
    urlutils,
 
29
    )
 
30
 
 
31
 
 
32
class MailClient(object):
 
33
    """A mail client that can send messages with attachements."""
 
34
 
 
35
    def __init__(self, config):
 
36
        self.config = config
 
37
 
 
38
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
39
                extension):
 
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
        """
 
55
        raise NotImplementedError
 
56
 
 
57
    def compose_merge_request(self, to, subject, directive):
 
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
        """
 
65
        prompt = self._get_merge_prompt("Please describe these changes:", to,
 
66
                                        subject, directive)
 
67
        self.compose(prompt, to, subject, directive,
 
68
            'x-patch', '.patch')
 
69
 
 
70
    def _get_merge_prompt(self, prompt, to, subject, attachment):
 
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
        """
 
78
        return ''
 
79
 
 
80
 
 
81
class Editor(MailClient):
 
82
    """DIY mail client that uses commit message editor"""
 
83
 
 
84
    def _get_merge_prompt(self, prompt, to, subject, attachment):
 
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')))
 
91
 
 
92
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
93
                extension):
 
94
        """See MailClient.compose"""
 
95
        body = msgeditor.edit_commit_message(prompt)
 
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,
 
104
                                        attachment_mime_subtype=mime_subtype)
 
105
 
 
106
 
 
107
class ExternalMailClient(MailClient):
 
108
    """An external mail client."""
 
109
 
 
110
    def _get_client_commands(self):
 
111
        """Provide a list of commands that may invoke the mail client"""
 
112
        if sys.platform == 'win32':
 
113
            import win32utils
 
114
            return [win32utils.get_app_path(i) for i in self._client_commands]
 
115
        else:
 
116
            return self._client_commands
 
117
 
 
118
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
119
                extension):
 
120
        """See MailClient.compose.
 
121
 
 
122
        Writes the attachment to a temporary file, invokes _compose.
 
123
        """
 
124
        fd, pathname = tempfile.mkstemp(extension, 'bzr-mail-')
 
125
        try:
 
126
            os.write(fd, attachment)
 
127
        finally:
 
128
            os.close(fd)
 
129
        self._compose(prompt, to, subject, pathname, mime_subtype, extension)
 
130
 
 
131
    def _compose(self, prompt, to, subject, attach_path, mime_subtype,
 
132
                extension):
 
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
        """
 
144
        for name in self._get_client_commands():
 
145
            cmdline = [name]
 
146
            cmdline.extend(self._get_compose_commandline(to, subject,
 
147
                                                         attach_path))
 
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):
 
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
        """
 
166
        raise NotImplementedError
 
167
 
 
168
 
 
169
class Evolution(ExternalMailClient):
 
170
    """Evolution mail client."""
 
171
 
 
172
    _client_commands = ['evolution']
 
173
 
 
174
    def _get_compose_commandline(self, to, subject, attach_path):
 
175
        """See ExternalMailClient._get_compose_commandline"""
 
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):
 
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
 
 
196
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
 
197
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
 
198
 
 
199
    def _get_compose_commandline(self, to, subject, attach_path):
 
200
        """See ExternalMailClient._get_compose_commandline"""
 
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)]
 
212
 
 
213
 
 
214
class KMail(ExternalMailClient):
 
215
    """KDE mail client."""
 
216
 
 
217
    _client_commands = ['kmail']
 
218
 
 
219
    def _get_compose_commandline(self, to, subject, attach_path):
 
220
        """See ExternalMailClient._get_compose_commandline"""
 
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
 
 
232
class XDGEmail(ExternalMailClient):
 
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):
 
238
        """See ExternalMailClient._get_compose_commandline"""
 
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
 
245
 
 
246
 
 
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):
 
252
        """See ExternalMailClient._compose.
 
253
 
 
254
        This implementation uses MAPI via the simplemapi ctypes wrapper
 
255
        """
 
256
        from bzrlib.util import simplemapi
 
257
        try:
 
258
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
 
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,)])
 
263
 
 
264
 
 
265
class DefaultMail(MailClient):
 
266
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
267
    falls back to Editor"""
 
268
 
 
269
    def _mail_client(self):
 
270
        """Determine the preferred mail client for this platform"""
 
271
        if osutils.supports_mapi():
 
272
            return MAPIClient(self.config)
 
273
        else:
 
274
            return XDGEmail(self.config)
 
275
 
 
276
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
277
                extension):
 
278
        """See MailClient.compose"""
 
279
        try:
 
280
            return self._mail_client().compose(prompt, to, subject,
 
281
                                               attachment, mimie_subtype,
 
282
                                               extension)
 
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):
 
288
        """See MailClient.compose_merge_request"""
 
289
        try:
 
290
            return self._mail_client().compose_merge_request(to, subject,
 
291
                                                             directive)
 
292
        except errors.MailClientNotFound:
 
293
            return Editor(self.config).compose_merge_request(to, subject,
 
294
                          directive)