~bzr/bzr-email/trunk

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
57.1.1 by Robert J. Tanner
Fix for Bug #983983 https://bugs.launchpad.net/bzr-email/+bug/983983
32
from bzrlib.email_message import EmailMessage
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
33
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
34
35
class EmailSender(object):
36
    """An email message sender."""
37
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
38
    _smtplib_implementation = SMTPConnection
39
39 by Robert Collins
Draft support for mailing on push/pull.
40
    def __init__(self, branch, revision_id, config, local_branch=None,
41
        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.
42
        self.config = config
43
        self.branch = branch
24 by John Arbash Meinel
Switch to using a local repository if available,
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
39 by Robert Collins
Draft support for mailing on push/pull.
51
        self.op = op
24 by John Arbash Meinel
Switch to using a local repository if available,
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)
51 by Jelmer Vernooij
Avoid double lookup of revision, remove some unnecessary whitespace.
56
40.4.4 by Renato Silva
Small code fixes requested by Jelmer Vernooij for merging
57
    def _format(self, text):
40.3.1 by Renato Silva
New user options for email subject and body.
58
        fields = {
51 by Jelmer Vernooij
Avoid double lookup of revision, remove some unnecessary whitespace.
59
            'committer': self.revision.committer,
40.3.1 by Renato Silva
New user options for email subject and body.
60
            'message': self.revision.get_summary(),
51 by Jelmer Vernooij
Avoid double lookup of revision, remove some unnecessary whitespace.
61
            'revision': '%d' % self.revno,
62
            'url': self.url()
63
        }
40.3.1 by Renato Silva
New user options for email subject and body.
64
        for name, value in fields.items():
51 by Jelmer Vernooij
Avoid double lookup of revision, remove some unnecessary whitespace.
65
            text = text.replace('$%s' % name, value)
40.4.4 by Renato Silva
Small code fixes requested by Jelmer Vernooij for merging
66
        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.
67
68
    def body(self):
34 by John Arbash Meinel
Special case showing a single revision without a merge.
69
        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.
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
16.1.11 by John Arbash Meinel
Cleanup from review comments by Marius Gedminas
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.
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
81
        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.
82
        outf = StringIO()
16.1.13 by John Arbash Meinel
Add an entry in the body to make it easier to find the url
83
55 by Jelmer Vernooij
Use config stacks.
84
        _body = self.config.get('post_commit_body')
40.4.4 by Renato Silva
Small code fixes requested by Jelmer Vernooij for merging
85
        if _body is None:
51 by Jelmer Vernooij
Avoid double lookup of revision, remove some unnecessary whitespace.
86
            _body = 'At %s\n\n' % self.url()
40.4.4 by Renato Silva
Small code fixes requested by Jelmer Vernooij for merging
87
        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
88
55 by Jelmer Vernooij
Use config stacks.
89
        log_format = self.config.get('post_commit_log_format')
90
        lf = log.log_formatter(log_format,
34 by John Arbash Meinel
Special case showing a single revision without a merge.
91
                               show_ids=True,
92
                               to_file=outf
93
                               )
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
94
34 by John Arbash Meinel
Special case showing a single revision without a merge.
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
                         )
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
109
110
        return outf.getvalue()
111
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
112
    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.
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
        """
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
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
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
122
        from bzrlib.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]
24 by John Arbash Meinel
Switch to using a local repository if available,
127
            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.
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
24 by John Arbash Meinel
Switch to using a local repository if available,
132
            tree_new = self.repository.revision_tree(revid_new)
133
            tree_old = self.repository.revision_tree(revid_old)
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
134
16.1.11 by John Arbash Meinel
Cleanup from review comments by Marius Gedminas
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.
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
137
        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.
138
        diff_content = StringIO()
55 by Jelmer Vernooij
Use config stacks.
139
        diff_options = self.config.get('post_commit_diffoptions')
44.2.1 by Sergei Golubchik
add support for post_commit_diffoptions
140
        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
141
        numlines = diff_content.getvalue().count('\n')+1
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
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))
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
148
149
    def difflimit(self):
16.1.11 by John Arbash Meinel
Cleanup from review comments by Marius Gedminas
150
        """Maximum number of lines of diff to show."""
55 by Jelmer Vernooij
Use config stacks.
151
        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.
152
153
    def mailer(self):
154
        """What mail program to use."""
55 by Jelmer Vernooij
Use config stacks.
155
        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.
156
157
    def _command_line(self):
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
158
        cmd = [self.mailer(), '-s', self.subject(), '-a',
159
                "From: " + self.from_address()]
55 by Jelmer Vernooij
Use config stacks.
160
        cmd.extend(self.to())
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
161
        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.
162
163
    def to(self):
164
        """What is the address the mail should go to."""
55 by Jelmer Vernooij
Use config stacks.
165
        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.
166
167
    def url(self):
168
        """What URL to display in the subject of the mail"""
55 by Jelmer Vernooij
Use config stacks.
169
        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.
170
        if url is None:
55 by Jelmer Vernooij
Use config stacks.
171
            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.
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."""
55 by Jelmer Vernooij
Use config stacks.
178
        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.
179
        if result is None:
55 by Jelmer Vernooij
Use config stacks.
180
            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.
181
        return result
182
41.1.1 by Steve Langasek
add support for a new option, 'post_commit_headers', used to specify
183
    def extra_headers(self):
184
        """Additional headers to include when sending."""
185
        result = {}
55 by Jelmer Vernooij
Use config stacks.
186
        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.
187
        if not headers:
188
            return
41.1.2 by Steve Langasek
whoops, don't iterate over the characters in the string when there's only one
189
        for line in headers:
42 by Robert Collins
Merge patch from Steve Langasek adding support for arbitrary headers on revision notification emails.
190
            key, value = line.split(": ", 1)
191
            result[key] = value
41.1.1 by Steve Langasek
add support for a new option, 'post_commit_headers', used to specify
192
        return result
193
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
194
    def send(self):
16.1.4 by John Arbash Meinel
Add a class for handling emails properly.
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
        """
24 by John Arbash Meinel
Switch to using a local repository if available,
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()
16.1.4 by John Arbash Meinel
Add a class for handling emails properly.
213
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
214
    def _send_using_process(self):
16.1.4 by John Arbash Meinel
Add a class for handling emails properly.
215
        """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.
216
        # TODO think up a good test for this, but I think it needs
217
        # 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
218
        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.
219
        try:
37.1.1 by Martin Pool
Change subprocess mail-sender to use a tempfile rather than a pipe
220
            msgfile.write(self.body().encode('utf8'))
46 by Martin Pool
Write outgoing message to a file to try to avoid deadlocks
221
            diff = self.get_diff()
222
            if diff:
223
                msgfile.write(diff)
37.1.1 by Martin Pool
Change subprocess mail-sender to use a tempfile rather than a pipe
224
            msgfile.flush()
225
            msgfile.seek(0)
226
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
227
            process = subprocess.Popen(self._command_line(),
37.1.1 by Martin Pool
Change subprocess mail-sender to use a tempfile rather than a pipe
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()
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
235
16.1.4 by John Arbash Meinel
Add a class for handling emails properly.
236
    def _send_using_smtplib(self):
237
        """Use python's smtplib to send the email."""
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
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]
57.1.1 by Robert J. Tanner
Fix for Bug #983983 https://bugs.launchpad.net/bzr-email/+bug/983983
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())
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
251
57.1.1 by Robert J. Tanner
Fix for Bug #983983 https://bugs.launchpad.net/bzr-email/+bug/983983
252
        # Add revision_mail_headers to the headers
57.1.2 by Robert J. Tanner
Fixed issue revision_mail_headers not defined in .bazaar/bazaar.conf
253
        if header != None:
254
            for k, v in header.items():
255
                msg[k] = v
57.1.1 by Robert J. Tanner
Fix for Bug #983983 https://bugs.launchpad.net/bzr-email/+bug/983983
256
        
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
257
        smtp = self._smtplib_implementation(self.config)
57.1.1 by Robert J. Tanner
Fix for Bug #983983 https://bugs.launchpad.net/bzr-email/+bug/983983
258
        smtp.send_email(msg)
16.1.4 by John Arbash Meinel
Add a class for handling emails properly.
259
16.1.3 by John Arbash Meinel
Move guts into another file to improve startup time, fix bug when old revid is None.
260
    def should_send(self):
55 by Jelmer Vernooij
Use config stacks.
261
        post_commit_push_pull = self.config.get('post_commit_push_pull')
39 by Robert Collins
Draft support for mailing on 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
33.1.2 by Vincent Ladeuil
Fixed as per Robert's review.
268
        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.
269
270
    def send_maybe(self):
271
        if self.should_send():
272
            self.send()
51 by Jelmer Vernooij
Avoid double lookup of revision, remove some unnecessary whitespace.
273
274
    def subject(self):
55 by Jelmer Vernooij
Use config stacks.
275
        _subject = self.config.get('post_commit_subject')
40.4.4 by Renato Silva
Small code fixes requested by Jelmer Vernooij for merging
276
        if _subject is None:
40.3.1 by Renato Silva
New user options for email subject and body.
277
            _subject = ("Rev %d: %s in %s" % 
40.3.3 by Renato Silva
Removed external references for fields dict.
278
                (self.revno,
279
                 self.revision.get_summary(),
280
                 self.url()))
40.4.4 by Renato Silva
Small code fixes requested by Jelmer Vernooij for merging
281
        return self._format(_subject)
16.1.4 by John Arbash Meinel
Add a class for handling emails properly.
282
16.1.7 by John Arbash Meinel
split out SMTPConnection to its own file.
283
    def diff_filename(self):
284
        return "patch-%s.diff" % (self.revno,)
285
55 by Jelmer Vernooij
Use config stacks.
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.")