/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-10-05 02:41:37 UTC
  • mto: (2592.3.166 repository)
  • mto: This revision was merged to the branch mainline in revision 2896.
  • Revision ID: robertc@robertcollins.net-20071005024137-kn7brcu07nu8cwl1
* The class ``bzrlib.repofmt.knitrepo.KnitRepository3`` has been folded into
  ``KnitRepository`` by parameters to the constructor. (Robert Collins)
* ``bzrlib.xml_serializer.Serializer`` is now responsible for checking that
  mandatory attributes are present on serialisation and deserialisation.
  This fixes some holes in API usage and allows better separation between
  physical storage and object serialisation. (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 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
 
 
203
class Thunderbird(ExternalMailClient):
 
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
 
 
213
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
 
214
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin']
 
215
 
 
216
    def _get_compose_commandline(self, to, subject, attach_path):
 
217
        """See ExternalMailClient._get_compose_commandline"""
 
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)]
 
229
 
 
230
 
 
231
class KMail(ExternalMailClient):
 
232
    """KDE mail client."""
 
233
 
 
234
    _client_commands = ['kmail']
 
235
 
 
236
    def _get_compose_commandline(self, to, subject, attach_path):
 
237
        """See ExternalMailClient._get_compose_commandline"""
 
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
 
 
249
class XDGEmail(ExternalMailClient):
 
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):
 
255
        """See ExternalMailClient._get_compose_commandline"""
 
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
 
262
 
 
263
 
 
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):
 
269
        """See ExternalMailClient._compose.
 
270
 
 
271
        This implementation uses MAPI via the simplemapi ctypes wrapper
 
272
        """
 
273
        from bzrlib.util import simplemapi
 
274
        try:
 
275
            simplemapi.SendMail(to or '', subject or '', '', attach_path)
 
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,)])
 
280
 
 
281
 
 
282
class DefaultMail(MailClient):
 
283
    """Default mail handling.  Tries XDGEmail (or MAPIClient on Windows),
 
284
    falls back to Editor"""
 
285
 
 
286
    def _mail_client(self):
 
287
        """Determine the preferred mail client for this platform"""
 
288
        if osutils.supports_mapi():
 
289
            return MAPIClient(self.config)
 
290
        else:
 
291
            return XDGEmail(self.config)
 
292
 
 
293
    def compose(self, prompt, to, subject, attachment, mime_subtype,
 
294
                extension):
 
295
        """See MailClient.compose"""
 
296
        try:
 
297
            return self._mail_client().compose(prompt, to, subject,
 
298
                                               attachment, mimie_subtype,
 
299
                                               extension)
 
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):
 
305
        """See MailClient.compose_merge_request"""
 
306
        try:
 
307
            return self._mail_client().compose_merge_request(to, subject,
 
308
                                                             directive)
 
309
        except errors.MailClientNotFound:
 
310
            return Editor(self.config).compose_merge_request(to, subject,
 
311
                          directive)