/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: John Arbash Meinel
  • Date: 2006-04-25 15:05:42 UTC
  • mfrom: (1185.85.85 bzr-encoding)
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: john@arbash-meinel.com-20060425150542-c7b518dca9928691
[merge] the old bzr-encoding changes, reparenting them on bzr.dev

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