/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 breezy/mail_client.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import subprocess
20
20
import sys
21
21
import tempfile
22
 
import urllib
23
22
 
24
 
import bzrlib
25
 
from bzrlib import (
 
23
import breezy
 
24
from . import (
 
25
    config as _mod_config,
26
26
    email_message,
27
27
    errors,
28
28
    msgeditor,
29
29
    osutils,
30
30
    urlutils,
31
 
    registry
 
31
    registry,
32
32
    )
33
33
 
34
34
mail_client_registry = registry.Registry()
35
35
 
36
36
 
 
37
class MailClientNotFound(errors.BzrError):
 
38
 
 
39
    _fmt = "Unable to find mail client with the following names:"\
 
40
        " %(mail_command_list_string)s"
 
41
 
 
42
    def __init__(self, mail_command_list):
 
43
        mail_command_list_string = ', '.join(mail_command_list)
 
44
        errors.BzrError.__init__(
 
45
            self, mail_command_list=mail_command_list,
 
46
            mail_command_list_string=mail_command_list_string)
 
47
 
 
48
 
 
49
class NoMessageSupplied(errors.BzrError):
 
50
 
 
51
    _fmt = "No message supplied."
 
52
 
 
53
 
 
54
class NoMailAddressSpecified(errors.BzrError):
 
55
 
 
56
    _fmt = "No mail-to address (--mail-to) or output (-o) specified."
 
57
 
 
58
 
37
59
class MailClient(object):
38
60
    """A mail client that can send messages with attachements."""
39
61
 
75
97
        prompt = self._get_merge_prompt("Please describe these changes:", to,
76
98
                                        subject, directive)
77
99
        self.compose(prompt, to, subject, directive,
78
 
            'x-patch', '.patch', basename, body)
 
100
                     'x-patch', '.patch', basename, body)
79
101
 
80
102
    def _get_merge_prompt(self, prompt, to, subject, attachment):
81
103
        """Generate a prompt string.  Overridden by Editor.
105
127
                extension, basename=None, body=None):
106
128
        """See MailClient.compose"""
107
129
        if not to:
108
 
            raise errors.NoMailAddressSpecified()
 
130
            raise NoMailAddressSpecified()
109
131
        body = msgeditor.edit_commit_message(prompt, start_message=body)
110
132
        if body == '':
111
 
            raise errors.NoMessageSupplied()
 
133
            raise NoMessageSupplied()
112
134
        email_message.EmailMessage.send(self.config,
113
 
                                        self.config.username(),
 
135
                                        self.config.get('email'),
114
136
                                        to,
115
137
                                        subject,
116
138
                                        body,
117
139
                                        attachment,
118
140
                                        attachment_mime_subtype=mime_subtype)
 
141
 
 
142
 
119
143
mail_client_registry.register('editor', Editor,
120
144
                              help=Editor.__doc__)
121
145
 
142
166
            basename = 'attachment'
143
167
        pathname = osutils.mkdtemp(prefix='bzr-mail-')
144
168
        attach_path = osutils.pathjoin(pathname, basename + extension)
145
 
        outfile = open(attach_path, 'wb')
146
 
        try:
 
169
        with open(attach_path, 'wb') as outfile:
147
170
            outfile.write(attachment)
148
 
        finally:
149
 
            outfile.close()
150
171
        if body is not None:
151
172
            kwargs = {'body': body}
152
173
        else:
182
203
                                                         **kwargs))
183
204
            try:
184
205
                subprocess.call(cmdline)
185
 
            except OSError, e:
 
206
            except OSError as e:
186
207
                if e.errno != errno.ENOENT:
187
208
                    raise
188
209
            else:
189
210
                break
190
211
        else:
191
 
            raise errors.MailClientNotFound(self._client_commands)
 
212
            raise MailClientNotFound(self._client_commands)
192
213
 
193
214
    def _get_compose_commandline(self, to, subject, attach_path, body):
194
215
        """Determine the commandline to use for composing a message
208
229
        :param  u:  possible unicode string.
209
230
        :return:    encoded string if u is unicode, u itself otherwise.
210
231
        """
211
 
        if isinstance(u, unicode):
212
 
            return u.encode(osutils.get_user_encoding(), 'replace')
213
232
        return u
214
233
 
215
234
    def _encode_path(self, path, kind):
221
240
                        path itself otherwise.
222
241
        :raise:         UnableEncodePath.
223
242
        """
224
 
        if isinstance(path, unicode):
225
 
            try:
226
 
                return path.encode(osutils.get_user_encoding())
227
 
            except UnicodeEncodeError:
228
 
                raise errors.UnableEncodePath(path, kind)
229
243
        return path
230
244
 
231
245
 
250
264
        if body is not None:
251
265
            message_options['body'] = body
252
266
        options_list = ['%s=%s' % (k, urlutils.escape(v)) for (k, v) in
253
 
                        sorted(message_options.iteritems())]
 
267
                        sorted(message_options.items())]
254
268
        return ['mailto:%s?%s' % (self._encode_safe(to or ''),
255
 
            '&'.join(options_list))]
 
269
                                  '&'.join(options_list))]
 
270
 
 
271
 
256
272
mail_client_registry.register('evolution', Evolution,
257
273
                              help=Evolution.__doc__)
258
274
 
266
282
        """See ExternalMailClient._get_compose_commandline"""
267
283
        message_options = []
268
284
        if subject is not None:
269
 
            message_options.extend(['-s', self._encode_safe(subject)])
 
285
            message_options.extend(
 
286
                ['-s', self._encode_safe(subject)])
270
287
        if attach_path is not None:
271
 
            message_options.extend(['-a',
272
 
                self._encode_path(attach_path, 'attachment')])
 
288
            message_options.extend(
 
289
                ['-a', self._encode_path(attach_path, 'attachment')])
273
290
        if body is not None:
274
291
            # Store the temp file object in self, so that it does not get
275
292
            # garbage collected and delete the file before mutt can read it.
276
293
            self._temp_file = tempfile.NamedTemporaryFile(
277
 
                prefix="mutt-body-", suffix=".txt")
 
294
                prefix="mutt-body-", suffix=".txt", mode="w+")
278
295
            self._temp_file.write(body)
279
296
            self._temp_file.flush()
280
297
            message_options.extend(['-i', self._temp_file.name])
281
298
        if to is not None:
282
299
            message_options.extend(['--', self._encode_safe(to)])
283
300
        return message_options
 
301
 
 
302
 
284
303
mail_client_registry.register('mutt', Mutt,
285
304
                              help=Mutt.__doc__)
286
305
 
295
314
    send attachments.
296
315
    """
297
316
 
298
 
    _client_commands = ['thunderbird', 'mozilla-thunderbird', 'icedove',
 
317
    _client_commands = [
 
318
        'thunderbird', 'mozilla-thunderbird', 'icedove',
299
319
        '/Applications/Mozilla/Thunderbird.app/Contents/MacOS/thunderbird-bin',
300
320
        '/Applications/Thunderbird.app/Contents/MacOS/thunderbird-bin']
301
321
 
310
330
            message_options['attachment'] = urlutils.local_path_to_url(
311
331
                attach_path)
312
332
        if body is not None:
313
 
            options_list = ['body=%s' % urllib.quote(self._encode_safe(body))]
 
333
            options_list = ['body=%s' %
 
334
                            urlutils.quote(self._encode_safe(body))]
314
335
        else:
315
336
            options_list = []
316
337
        options_list.extend(["%s='%s'" % (k, v) for k, v in
317
 
                        sorted(message_options.iteritems())])
 
338
                             sorted(message_options.items())])
318
339
        return ['-compose', ','.join(options_list)]
 
340
 
 
341
 
319
342
mail_client_registry.register('thunderbird', Thunderbird,
320
343
                              help=Thunderbird.__doc__)
321
344
 
331
354
        if subject is not None:
332
355
            message_options.extend(['-s', self._encode_safe(subject)])
333
356
        if attach_path is not None:
334
 
            message_options.extend(['--attach',
335
 
                self._encode_path(attach_path, 'attachment')])
 
357
            message_options.extend(
 
358
                ['--attach', self._encode_path(attach_path, 'attachment')])
336
359
        if to is not None:
337
360
            message_options.extend([self._encode_safe(to)])
338
361
        return message_options
 
362
 
 
363
 
339
364
mail_client_registry.register('kmail', KMail,
340
365
                              help=KMail.__doc__)
341
366
 
352
377
        """See ExternalMailClient._get_compose_commandline"""
353
378
        compose_url = []
354
379
        if from_ is not None:
355
 
            compose_url.append('from=' + urllib.quote(from_))
 
380
            compose_url.append('from=' + urlutils.quote(from_))
356
381
        if subject is not None:
357
 
            # Don't use urllib.quote_plus because Claws doesn't seem
 
382
            # Don't use urlutils.quote_plus because Claws doesn't seem
358
383
            # to recognise spaces encoded as "+".
359
384
            compose_url.append(
360
 
                'subject=' + urllib.quote(self._encode_safe(subject)))
 
385
                'subject=' + urlutils.quote(self._encode_safe(subject)))
361
386
        if body is not None:
362
387
            compose_url.append(
363
 
                'body=' + urllib.quote(self._encode_safe(body)))
 
388
                'body=' + urlutils.quote(self._encode_safe(body)))
364
389
        # to must be supplied for the claws-mail --compose syntax to work.
365
390
        if to is None:
366
 
            raise errors.NoMailAddressSpecified()
 
391
            raise NoMailAddressSpecified()
367
392
        compose_url = 'mailto:%s?%s' % (
368
393
            self._encode_safe(to), '&'.join(compose_url))
369
394
        # Collect command-line options.
377
402
                 extension, body=None, from_=None):
378
403
        """See ExternalMailClient._compose"""
379
404
        if from_ is None:
380
 
            from_ = self.config.get_user_option('email')
 
405
            from_ = self.config.get('email')
381
406
        super(Claws, self)._compose(prompt, to, subject, attach_path,
382
407
                                    mime_subtype, extension, body, from_)
383
408
 
387
412
 
388
413
 
389
414
class XDGEmail(BodyExternalMailClient):
390
 
    __doc__ = """xdg-email attempts to invoke the user's preferred mail client"""
 
415
    __doc__ = """xdg-email attempts to invoke the preferred mail client"""
391
416
 
392
417
    _client_commands = ['xdg-email']
393
418
 
394
419
    def _get_compose_commandline(self, to, subject, attach_path, body=None):
395
420
        """See ExternalMailClient._get_compose_commandline"""
396
421
        if not to:
397
 
            raise errors.NoMailAddressSpecified()
 
422
            raise NoMailAddressSpecified()
398
423
        commandline = [self._encode_safe(to)]
399
424
        if subject is not None:
400
425
            commandline.extend(['--subject', self._encode_safe(subject)])
401
426
        if attach_path is not None:
402
427
            commandline.extend(['--attach',
403
 
                self._encode_path(attach_path, 'attachment')])
 
428
                                self._encode_path(attach_path, 'attachment')])
404
429
        if body is not None:
405
430
            commandline.extend(['--body', self._encode_safe(body)])
406
431
        return commandline
 
432
 
 
433
 
407
434
mail_client_registry.register('xdg-email', XDGEmail,
408
435
                              help=XDGEmail.__doc__)
409
436
 
442
469
        after being read by Emacs.)
443
470
        """
444
471
 
445
 
        _defun = r"""(defun bzr-add-mime-att (file)
 
472
        _defun = br"""(defun bzr-add-mime-att (file)
446
473
  "Attach FILE to a mail buffer as a MIME attachment."
447
474
  (let ((agent mail-user-agent))
448
475
    (if (and file (file-exists-p file))
478
505
        try:
479
506
            os.write(fd, _defun)
480
507
        finally:
481
 
            os.close(fd) # Just close the handle but do not remove the file.
 
508
            os.close(fd)  # Just close the handle but do not remove the file.
482
509
        return temp_file
483
510
 
484
511
    def _get_compose_commandline(self, to, subject, attach_path):
506
533
            elisp = self._prepare_send_function()
507
534
            self.elisp_tmp_file = elisp
508
535
            lmmform = '(load "%s")' % elisp
509
 
            mmform  = '(bzr-add-mime-att "%s")' % \
 
536
            mmform = '(bzr-add-mime-att "%s")' % \
510
537
                self._encode_path(attach_path, 'attachment')
511
538
            rmform = '(delete-file "%s")' % elisp
512
539
            commandline.append(lmmform)
514
541
            commandline.append(rmform)
515
542
 
516
543
        return commandline
 
544
 
 
545
 
517
546
mail_client_registry.register('emacsclient', EmacsMail,
518
547
                              help=EmacsMail.__doc__)
519
548
 
527
556
 
528
557
        This implementation uses MAPI via the simplemapi ctypes wrapper
529
558
        """
530
 
        from bzrlib.util import simplemapi
 
559
        from .util import simplemapi
531
560
        try:
532
561
            simplemapi.SendMail(to or '', subject or '', body or '',
533
562
                                attach_path)
534
 
        except simplemapi.MAPIError, e:
 
563
        except simplemapi.MAPIError as e:
535
564
            if e.code != simplemapi.MAPI_USER_ABORT:
536
 
                raise errors.MailClientNotFound(['MAPI supported mail client'
537
 
                                                 ' (error %d)' % (e.code,)])
 
565
                raise MailClientNotFound(['MAPI supported mail client'
 
566
                                          ' (error %d)' % (e.code,)])
 
567
 
 
568
 
538
569
mail_client_registry.register('mapi', MAPIClient,
539
570
                              help=MAPIClient.__doc__)
540
571
 
552
583
    _client_commands = ['osascript']
553
584
 
554
585
    def _get_compose_commandline(self, to, subject, attach_path, body=None,
555
 
                                from_=None):
556
 
       """See ExternalMailClient._get_compose_commandline"""
557
 
 
558
 
       fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
559
 
                                         suffix=".scpt")
560
 
       try:
561
 
           os.write(fd, 'tell application "Mail"\n')
562
 
           os.write(fd, 'set newMessage to make new outgoing message\n')
563
 
           os.write(fd, 'tell newMessage\n')
564
 
           if to is not None:
565
 
               os.write(fd, 'make new to recipient with properties'
566
 
                   ' {address:"%s"}\n' % to)
567
 
           if from_ is not None:
568
 
               # though from_ doesn't actually seem to be used
569
 
               os.write(fd, 'set sender to "%s"\n'
570
 
                   % sender.replace('"', '\\"'))
571
 
           if subject is not None:
572
 
               os.write(fd, 'set subject to "%s"\n'
573
 
                   % subject.replace('"', '\\"'))
574
 
           if body is not None:
575
 
               # FIXME: would be nice to prepend the body to the
576
 
               # existing content (e.g., preserve signature), but
577
 
               # can't seem to figure out the right applescript
578
 
               # incantation.
579
 
               os.write(fd, 'set content to "%s\\n\n"\n' %
580
 
                   body.replace('"', '\\"').replace('\n', '\\n'))
581
 
 
582
 
           if attach_path is not None:
583
 
               # FIXME: would be nice to first append a newline to
584
 
               # ensure the attachment is on a new paragraph, but
585
 
               # can't seem to figure out the right applescript
586
 
               # incantation.
587
 
               os.write(fd, 'tell content to make new attachment'
588
 
                   ' with properties {file name:"%s"}'
589
 
                   ' at after the last paragraph\n'
590
 
                   % self._encode_path(attach_path, 'attachment'))
591
 
           os.write(fd, 'set visible to true\n')
592
 
           os.write(fd, 'end tell\n')
593
 
           os.write(fd, 'end tell\n')
594
 
       finally:
595
 
           os.close(fd) # Just close the handle but do not remove the file.
596
 
       return [self.temp_file]
 
586
                                 from_=None):
 
587
        """See ExternalMailClient._get_compose_commandline"""
 
588
 
 
589
        fd, self.temp_file = tempfile.mkstemp(prefix="bzr-send-",
 
590
                                              suffix=".scpt")
 
591
        try:
 
592
            os.write(fd, 'tell application "Mail"\n')
 
593
            os.write(fd, 'set newMessage to make new outgoing message\n')
 
594
            os.write(fd, 'tell newMessage\n')
 
595
            if to is not None:
 
596
                os.write(fd, 'make new to recipient with properties'
 
597
                         ' {address:"%s"}\n' % to)
 
598
            if from_ is not None:
 
599
                # though from_ doesn't actually seem to be used
 
600
                os.write(fd, 'set sender to "%s"\n'
 
601
                         % from_.replace('"', '\\"'))
 
602
            if subject is not None:
 
603
                os.write(fd, 'set subject to "%s"\n'
 
604
                         % subject.replace('"', '\\"'))
 
605
            if body is not None:
 
606
                # FIXME: would be nice to prepend the body to the
 
607
                # existing content (e.g., preserve signature), but
 
608
                # can't seem to figure out the right applescript
 
609
                # incantation.
 
610
                os.write(fd, 'set content to "%s\\n\n"\n' %
 
611
                         body.replace('"', '\\"').replace('\n', '\\n'))
 
612
 
 
613
            if attach_path is not None:
 
614
                # FIXME: would be nice to first append a newline to
 
615
                # ensure the attachment is on a new paragraph, but
 
616
                # can't seem to figure out the right applescript
 
617
                # incantation.
 
618
                os.write(fd, 'tell content to make new attachment'
 
619
                         ' with properties {file name:"%s"}'
 
620
                         ' at after the last paragraph\n'
 
621
                         % self._encode_path(attach_path, 'attachment'))
 
622
            os.write(fd, 'set visible to true\n')
 
623
            os.write(fd, 'end tell\n')
 
624
            os.write(fd, 'end tell\n')
 
625
        finally:
 
626
            os.close(fd)  # Just close the handle but do not remove the file.
 
627
        return [self.temp_file]
 
628
 
 
629
 
597
630
mail_client_registry.register('mail.app', MailApp,
598
631
                              help=MailApp.__doc__)
599
632
 
616
649
        """See MailClient.compose"""
617
650
        try:
618
651
            return self._mail_client().compose(prompt, to, subject,
619
 
                                               attachment, mimie_subtype,
 
652
                                               attachment, mime_subtype,
620
653
                                               extension, basename, body)
621
 
        except errors.MailClientNotFound:
622
 
            return Editor(self.config).compose(prompt, to, subject,
623
 
                          attachment, mimie_subtype, extension, body)
 
654
        except MailClientNotFound:
 
655
            return Editor(self.config).compose(
 
656
                prompt, to, subject, attachment, mime_subtype, extension, body)
624
657
 
625
658
    def compose_merge_request(self, to, subject, directive, basename=None,
626
659
                              body=None):
627
660
        """See MailClient.compose_merge_request"""
628
661
        try:
629
 
            return self._mail_client().compose_merge_request(to, subject,
630
 
                    directive, basename=basename, body=body)
631
 
        except errors.MailClientNotFound:
632
 
            return Editor(self.config).compose_merge_request(to, subject,
633
 
                          directive, basename=basename, body=body)
634
 
mail_client_registry.register('default', DefaultMail,
 
662
            return self._mail_client().compose_merge_request(
 
663
                to, subject, directive, basename=basename, body=body)
 
664
        except MailClientNotFound:
 
665
            return Editor(self.config).compose_merge_request(
 
666
                to, subject, directive, basename=basename, body=body)
 
667
 
 
668
 
 
669
mail_client_registry.register(u'default', DefaultMail,
635
670
                              help=DefaultMail.__doc__)
636
 
mail_client_registry.default_key = 'default'
637
 
 
638
 
 
 
671
mail_client_registry.default_key = u'default'
 
672
 
 
673
opt_mail_client = _mod_config.RegistryOption(
 
674
    'mail_client', mail_client_registry, help='E-mail client to use.',
 
675
    invalid='error')