~maria-captains/bzr-email/serg

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