/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/plugins/email/emailer.py

  • Committer: Jelmer Vernooij
  • Date: 2017-06-02 21:15:03 UTC
  • mfrom: (0.171.58 trunk)
  • mto: This revision was merged to the branch mainline in revision 6657.
  • Revision ID: jelmer@jelmer.uk-20170602211503-ba7ky35ukdpsxir1
Merge bzr-email plugin.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 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 subprocess
 
18
import tempfile
 
19
 
 
20
from ... import (
 
21
    errors,
 
22
    revision as _mod_revision,
 
23
    )
 
24
from ...config import (
 
25
    ListOption,
 
26
    Option,
 
27
    bool_from_store,
 
28
    int_from_store,
 
29
    )
 
30
 
 
31
from ...smtp_connection import SMTPConnection
 
32
from ...email_message import EmailMessage
 
33
 
 
34
 
 
35
class EmailSender(object):
 
36
    """An email message sender."""
 
37
 
 
38
    _smtplib_implementation = SMTPConnection
 
39
 
 
40
    def __init__(self, branch, revision_id, config, local_branch=None,
 
41
        op='commit'):
 
42
        self.config = config
 
43
        self.branch = branch
 
44
        self.repository = branch.repository
 
45
        if (local_branch is not None and
 
46
            local_branch.repository.has_revision(revision_id)):
 
47
            self.repository = local_branch.repository
 
48
        self._revision_id = revision_id
 
49
        self.revision = None
 
50
        self.revno = None
 
51
        self.op = op
 
52
 
 
53
    def _setup_revision_and_revno(self):
 
54
        self.revision = self.repository.get_revision(self._revision_id)
 
55
        self.revno = self.branch.revision_id_to_revno(self._revision_id)
 
56
 
 
57
    def _format(self, text):
 
58
        fields = {
 
59
            'committer': self.revision.committer,
 
60
            'message': self.revision.get_summary(),
 
61
            'revision': '%d' % self.revno,
 
62
            'url': self.url()
 
63
        }
 
64
        for name, value in fields.items():
 
65
            text = text.replace('$%s' % name, value)
 
66
        return text
 
67
 
 
68
    def body(self):
 
69
        from ... import log
 
70
 
 
71
        rev1 = rev2 = self.revno
 
72
        if rev1 == 0:
 
73
            rev1 = None
 
74
            rev2 = None
 
75
 
 
76
        # use 'replace' so that we don't abort if trying to write out
 
77
        # in e.g. the default C locale.
 
78
 
 
79
        # We must use StringIO.StringIO because we want a Unicode string that
 
80
        # we can pass to send_email and have that do the proper encoding.
 
81
        from StringIO import StringIO
 
82
        outf = StringIO()
 
83
 
 
84
        _body = self.config.get('post_commit_body')
 
85
        if _body is None:
 
86
            _body = 'At %s\n\n' % self.url()
 
87
        outf.write(self._format(_body))
 
88
 
 
89
        log_format = self.config.get('post_commit_log_format')
 
90
        lf = log.log_formatter(log_format,
 
91
                               show_ids=True,
 
92
                               to_file=outf
 
93
                               )
 
94
 
 
95
        if len(self.revision.parent_ids) <= 1:
 
96
            # This is not a merge, so we can special case the display of one
 
97
            # revision, and not have to encur the show_log overhead.
 
98
            lr = log.LogRevision(self.revision, self.revno, 0, None)
 
99
            lf.log_revision(lr)
 
100
        else:
 
101
            # let the show_log code figure out what revisions need to be
 
102
            # displayed, as this is a merge
 
103
            log.show_log(self.branch,
 
104
                         lf,
 
105
                         start_revision=rev1,
 
106
                         end_revision=rev2,
 
107
                         verbose=True
 
108
                         )
 
109
 
 
110
        return outf.getvalue()
 
111
 
 
112
    def get_diff(self):
 
113
        """Add the diff from the commit to the output.
 
114
 
 
115
        If the diff has more than difflimit lines, it will be skipped.
 
116
        """
 
117
        difflimit = self.difflimit()
 
118
        if not difflimit:
 
119
            # No need to compute a diff if we aren't going to display it
 
120
            return
 
121
 
 
122
        from ...diff import show_diff_trees
 
123
        # optionally show the diff if its smaller than the post_commit_difflimit option
 
124
        revid_new = self.revision.revision_id
 
125
        if self.revision.parent_ids:
 
126
            revid_old = self.revision.parent_ids[0]
 
127
            tree_new, tree_old = self.repository.revision_trees((revid_new, revid_old))
 
128
        else:
 
129
            # revision_trees() doesn't allow None or 'null:' to be passed as a
 
130
            # revision. So we need to call revision_tree() twice.
 
131
            revid_old = _mod_revision.NULL_REVISION
 
132
            tree_new = self.repository.revision_tree(revid_new)
 
133
            tree_old = self.repository.revision_tree(revid_old)
 
134
 
 
135
        # We can use a cStringIO because show_diff_trees should only write
 
136
        # 8-bit strings. It is an error to write a Unicode string here.
 
137
        from cStringIO import StringIO
 
138
        diff_content = StringIO()
 
139
        diff_options = self.config.get('post_commit_diffoptions')
 
140
        show_diff_trees(tree_old, tree_new, diff_content, None, diff_options)
 
141
        numlines = diff_content.getvalue().count('\n')+1
 
142
        if numlines <= difflimit:
 
143
            return diff_content.getvalue()
 
144
        else:
 
145
            return ("\nDiff too large for email"
 
146
                    " (%d lines, the limit is %d).\n"
 
147
                    % (numlines, difflimit))
 
148
 
 
149
    def difflimit(self):
 
150
        """Maximum number of lines of diff to show."""
 
151
        return self.config.get('post_commit_difflimit')
 
152
 
 
153
    def mailer(self):
 
154
        """What mail program to use."""
 
155
        return self.config.get('post_commit_mailer')
 
156
 
 
157
    def _command_line(self):
 
158
        cmd = [self.mailer(), '-s', self.subject(), '-a',
 
159
                "From: " + self.from_address()]
 
160
        cmd.extend(self.to())
 
161
        return cmd
 
162
 
 
163
    def to(self):
 
164
        """What is the address the mail should go to."""
 
165
        return self.config.get('post_commit_to')
 
166
 
 
167
    def url(self):
 
168
        """What URL to display in the subject of the mail"""
 
169
        url = self.config.get('post_commit_url')
 
170
        if url is None:
 
171
            url = self.config.get('public_branch')
 
172
        if url is None:
 
173
            url = self.branch.base
 
174
        return url
 
175
 
 
176
    def from_address(self):
 
177
        """What address should I send from."""
 
178
        result = self.config.get('post_commit_sender')
 
179
        if result is None:
 
180
            result = self.config.get('email')
 
181
        return result
 
182
 
 
183
    def extra_headers(self):
 
184
        """Additional headers to include when sending."""
 
185
        result = {}
 
186
        headers = self.config.get('revision_mail_headers')
 
187
        if not headers:
 
188
            return
 
189
        for line in headers:
 
190
            key, value = line.split(": ", 1)
 
191
            result[key] = value
 
192
        return result
 
193
 
 
194
    def send(self):
 
195
        """Send the email.
 
196
 
 
197
        Depending on the configuration, this will either use smtplib, or it
 
198
        will call out to the 'mail' program.
 
199
        """
 
200
        self.branch.lock_read()
 
201
        self.repository.lock_read()
 
202
        try:
 
203
            # Do this after we have locked, to make things faster.
 
204
            self._setup_revision_and_revno()
 
205
            mailer = self.mailer()
 
206
            if mailer == 'smtplib':
 
207
                self._send_using_smtplib()
 
208
            else:
 
209
                self._send_using_process()
 
210
        finally:
 
211
            self.repository.unlock()
 
212
            self.branch.unlock()
 
213
 
 
214
    def _send_using_process(self):
 
215
        """Spawn a 'mail' subprocess to send the email."""
 
216
        # TODO think up a good test for this, but I think it needs
 
217
        # a custom binary shipped with. RBC 20051021
 
218
        msgfile = tempfile.NamedTemporaryFile()
 
219
        try:
 
220
            msgfile.write(self.body().encode('utf8'))
 
221
            diff = self.get_diff()
 
222
            if diff:
 
223
                msgfile.write(diff)
 
224
            msgfile.flush()
 
225
            msgfile.seek(0)
 
226
 
 
227
            process = subprocess.Popen(self._command_line(),
 
228
                stdin=msgfile.fileno())
 
229
 
 
230
            rc = process.wait()
 
231
            if rc != 0:
 
232
                raise errors.BzrError("Failed to send email: exit status %s" % (rc,))
 
233
        finally:
 
234
            msgfile.close()
 
235
 
 
236
    def _send_using_smtplib(self):
 
237
        """Use python's smtplib to send the email."""
 
238
        body = self.body()
 
239
        diff = self.get_diff()
 
240
        subject = self.subject()
 
241
        from_addr = self.from_address()
 
242
        to_addrs = self.to()
 
243
        if isinstance(to_addrs, basestring):
 
244
            to_addrs = [to_addrs]
 
245
        header = self.extra_headers()            
 
246
        
 
247
        msg = EmailMessage(from_addr, to_addrs, subject, body)
 
248
        
 
249
        if diff:
 
250
            msg.add_inline_attachment(diff, self.diff_filename())
 
251
 
 
252
        # Add revision_mail_headers to the headers
 
253
        if header != None:
 
254
            for k, v in header.items():
 
255
                msg[k] = v
 
256
        
 
257
        smtp = self._smtplib_implementation(self.config)
 
258
        smtp.send_email(msg)
 
259
 
 
260
    def should_send(self):
 
261
        post_commit_push_pull = self.config.get('post_commit_push_pull')
 
262
        if post_commit_push_pull and self.op == 'commit':
 
263
            # We will be called again with a push op, send the mail then.
 
264
            return False
 
265
        if not post_commit_push_pull and self.op != 'commit':
 
266
            # Mailing on commit only, and this is a push/pull operation.
 
267
            return False
 
268
        return bool(self.to() and self.from_address())
 
269
 
 
270
    def send_maybe(self):
 
271
        if self.should_send():
 
272
            self.send()
 
273
 
 
274
    def subject(self):
 
275
        _subject = self.config.get('post_commit_subject')
 
276
        if _subject is None:
 
277
            _subject = ("Rev %d: %s in %s" % 
 
278
                (self.revno,
 
279
                 self.revision.get_summary(),
 
280
                 self.url()))
 
281
        return self._format(_subject)
 
282
 
 
283
    def diff_filename(self):
 
284
        return "patch-%s.diff" % (self.revno,)
 
285
 
 
286
 
 
287
opt_post_commit_body = Option("post_commit_body",
 
288
    help="Body for post commit emails.")
 
289
opt_post_commit_subject = Option("post_commit_subject",
 
290
    help="Subject for post commit emails.")
 
291
opt_post_commit_log_format = Option('post_commit_log_format',
 
292
    default='long', help="Log format for option.")
 
293
opt_post_commit_difflimit = Option('post_commit_difflimit',
 
294
    default=1000, from_unicode=int_from_store,
 
295
    help="Maximum number of lines in diffs.")
 
296
opt_post_commit_push_pull = Option('post_commit_push_pull',
 
297
    from_unicode=bool_from_store,
 
298
    help="Whether to send emails on push and pull.")
 
299
opt_post_commit_diffoptions = Option('post_commit_diffoptions',
 
300
    help="Diff options to use.")
 
301
opt_post_commit_sender = Option('post_commit_sender',
 
302
    help='From address to use for emails.')
 
303
opt_post_commit_to = ListOption('post_commit_to',
 
304
    help='Address to send commit emails to.')
 
305
opt_post_commit_mailer = Option('post_commit_mailer',
 
306
    help='Mail client to use.', default='mail')
 
307
opt_post_commit_url = Option('post_commit_url',
 
308
    help='URL to mention for branch in post commit messages.')
 
309
opt_revision_mail_headers = ListOption('revision_mail_headers',
 
310
    help="Extra revision headers.")