~launchpad-pqm/launchpad/devel

13932.1.6 by mbp at canonical
process-one-mail arranges for sent mail to be printed to stdout.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
5925.1.4 by Curtis Hovey
Fixed copyrights.
3
11502.1.2 by Robert Collins
Record email sending in the request timeline.
4
"""The One True Way to send mail from the Launchpad application.
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
5
6061.16.1 by Curtis Hovey
Removed deprecation warning supression code. Updated ZCML, tests, and code to use
6
Uses zope.sendmail.interfaces.IMailer, so you can subscribe to
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
7
IMailSentEvent or IMailErrorEvent to record status.
8
9
TODO: We should append a signature to messages sent through
10
simple_sendmail and sendmail with a message explaining 'this
11
came from launchpad' and a link to click on to change their
12
messaging settings -- stub 2004-10-21
13
14
"""
15
2905.1.1 by Bjorn Tillenius
add simple_sendmail_from_person.
16
__all__ = [
10541.1.3 by Tim Penhey
Move the functions around again.
17
    'append_footer',
10921.3.1 by Diogo Matsubara
Fixes bug 504124 (process-mail.py "need more than 1 value to unpack")
18
    'do_paranoid_envelope_to_validation',
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
19
    'format_address',
7675.624.51 by Tim Penhey
Test passes now.
20
    'format_address_for_person',
6821.4.3 by Aaron Bentley
Ensure all branch merge proposal emails include an msg_id
21
    'get_msgid',
7325.7.1 by Aaron Bentley
Refactor into MailController
22
    'MailController',
2905.1.1 by Bjorn Tillenius
add simple_sendmail_from_person.
23
    'sendmail',
14022.2.6 by William Grant
Export set_immediate_mail_delivery.
24
    'set_immediate_mail_delivery',
2905.1.1 by Bjorn Tillenius
add simple_sendmail_from_person.
25
    'simple_sendmail',
26
    'simple_sendmail_from_person',
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
27
    'validate_message',
28
    ]
29
30
8918.2.3 by Aaron Bentley
Ensure line-endings are quoted when exact transfer is demanded.
31
from binascii import b2a_qp
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
32
from email import Charset
8918.2.1 by Aaron Bentley
Non-ascii attachments are base64 encoded.
33
from email.Encoders import encode_base64
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
34
from email.Header import Header
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
35
from email.Message import Message
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
36
from email.MIMEMultipart import MIMEMultipart
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
37
from email.MIMEText import MIMEText
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from email.Utils import (
39
    formataddr,
40
    formatdate,
41
    getaddresses,
42
    make_msgid,
43
    )
44
import hashlib
7675.624.86 by Tim Penhey
Fix the last broken test.
45
from smtplib import SMTP
13932.1.6 by mbp at canonical
process-one-mail arranges for sent mail to be printed to stdout.
46
import sys
1697 by Canonical.com Patch Queue Manager
Zopeless email [r=jamesh]
47
11502.1.2 by Robert Collins
Record email sending in the request timeline.
48
from lazr.restful.utils import get_current_browser_request
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
49
from zope.app import zapi
14027.3.2 by Jeroen Vermeulen
Merge devel, resolve conflicts.
50
from zope.security.proxy import (
51
    isinstance as zisinstance,
52
    removeSecurityProxy,
53
    )
6061.2.5 by Maris Fogels
Fixed many Zope DeprecationWarnings.
54
from zope.sendmail.interfaces import IMailDelivery
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
55
14612.2.1 by William Grant
format-imports on lib/. So many imports.
56
from lp.app import versioninfo
14605.1.1 by Curtis Hovey
Moved canonical.config to lp.services.
57
from lp.services.config import config
12767.2.3 by Tim Penhey
Move is_ascii_only into lp.services.encoding.
58
from lp.services.encoding import is_ascii_only
8377.6.14 by Tim Penhey
Move the StubMailer.
59
from lp.services.mail.stub import TestMailer
11502.1.2 by Robert Collins
Record email sending in the request timeline.
60
from lp.services.timeline.requesttimeline import get_request_timeline
1697 by Canonical.com Patch Queue Manager
Zopeless email [r=jamesh]
61
3473.1.1 by Stuart Bishop
Replace utf8 with utf-8 as per bug 39758. Untestable without refactoring.
62
# email package by default ends up encoding UTF-8 messages using base64,
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
63
# which sucks as they look like spam to stupid spam filters. We define
64
# our own custom charset definition to force quoted printable.
3473.1.3 by Stuart Bishop
Make email headers use shortest encoding rather than force quoted printable.
65
del Charset.CHARSETS['utf-8']
66
Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
67
Charset.add_alias('utf8', 'utf-8')
2161 by Canonical.com Patch Queue Manager
[r=stub] make sure simple_sendmail encodes the To and From headers properly if the names contain non-ascii characters.
68
14027.3.1 by Jeroen Vermeulen
Fix lots of lint in recently-changed files.
69
2821.2.3 by Brad Bollenbach
pre-review code cleanup
70
def do_paranoid_email_content_validation(from_addr, to_addrs, subject, body):
2821.2.1 by Brad Bollenbach
X-Malone-Bug, director's cut
71
    """Validate various bits of the email.
72
73
    Extremely paranoid parameter checking is required to ensure we
74
    raise an exception rather than stick garbage in the mail
75
    queue. Currently, the Z3 mailer is too forgiving and accepts badly
76
    formatted emails which the delivery mechanism then can't send.
2821.2.3 by Brad Bollenbach
pre-review code cleanup
77
78
    An AssertionError will be raised if one of the parameters is
79
    invalid.
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
80
    """
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
81
    # XXX StuartBishop 2005-03-19:
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
82
    # These checks need to be migrated upstream if this bug
83
    # still exists in modern Z3.
3564.7.13 by Diogo Matsubara
[r=salgado] Fix assert messages to interpolate with the right variables in sendmail.py
84
    assert zisinstance(from_addr, basestring), 'Invalid From: %r' % from_addr
85
    assert zisinstance(subject, basestring), 'Invalid Subject: %r' % subject
86
    assert zisinstance(body, basestring), 'Invalid body: %r' % body
8805.4.3 by Aaron Bentley
Move envelope_to validation to correct place.
87
88
89
def do_paranoid_envelope_to_validation(to_addrs):
90
    """Ensure the envelope_to addresses are valid.
91
92
    This is extracted from do_paranoid_email_content_validation, so that
93
    it can be applied to the actual envelope_to addresses, not the
94
    to header.  The to header and envelope_to addresses may vary
95
    independently, and the to header cannot break Z3.
96
    """
10293.3.1 by Max Bowsher
Remove use of the deprecated sets module.
97
    assert (zisinstance(to_addrs, (list, tuple, set))
8805.4.3 by Aaron Bentley
Move envelope_to validation to correct place.
98
            and len(to_addrs) > 0), 'Invalid To: %r' % (to_addrs,)
1495 by Canonical.com Patch Queue Manager
More paranoid email parameter checks and test suite fixes
99
    for addr in to_addrs:
2354 by Canonical.com Patch Queue Manager
[r=bjornt] Product and Project vocabularies should not contain inactive entries. Bug#1873
100
        assert zisinstance(addr, basestring) and bool(addr), \
1495 by Canonical.com Patch Queue Manager
More paranoid email parameter checks and test suite fixes
101
                'Invalid recipient: %r in %r' % (addr, to_addrs)
8137.4.3 by Bjorn Tillenius
Add a check for carriage returns in the paranoid e-mail checking.
102
        assert '\n' not in addr, (
8137.4.4 by Bjorn Tillenius
Typo.
103
            "Address contains carriage returns: %r" % (addr,))
8137.4.3 by Bjorn Tillenius
Add a check for carriage returns in the paranoid e-mail checking.
104
1208 by Canonical.com Patch Queue Manager
small code improvements based on code review and a db schema change to
105
10541.1.3 by Tim Penhey
Move the functions around again.
106
def append_footer(main, footer):
107
    """Append a footer to an email, following signature conventions.
108
109
    If there is no footer, do nothing.
110
    If there is already a signature, append an additional footer.
111
    If there is no existing signature, append '-- \n' and a footer.
112
113
    :param main: The main content, which may have a signature.
114
    :param footer: An additional footer to append.
115
    :return: a new version of main that includes the footer.
116
    """
117
    if footer == '':
118
        footer_separator = ''
119
    elif '\n-- \n' in main:
120
        footer_separator = '\n'
121
    else:
122
        footer_separator = '\n-- \n'
123
    return ''.join((main, footer_separator, footer))
124
125
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
126
def format_address(name, address):
127
    r"""Formats a name and address to be used as an email header.
128
129
        >>> format_address('Name', 'foo@bar.com')
130
        'Name <foo@bar.com>'
131
        >>> format_address('', 'foo@bar.com')
132
        'foo@bar.com'
3424.1.1 by Guilherme Salgado
Fix https://launchpad.net/bugs/28679 (Need email notifications when a person is approved/denied as a member of a team)
133
        >>> format_address(None, u'foo@bar.com')
134
        'foo@bar.com'
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
135
2949.2.8 by Bjorn Tillenius
tweaks.
136
    It handles unicode and characters that need quoting as well.
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
137
138
        >>> format_address(u'F\xf4\xf4 Bar', 'foo.bar@canonical.com')
139
        '=?utf-8?b?RsO0w7QgQmFy?= <foo.bar@canonical.com>'
140
141
        >>> format_address('Foo [Baz] Bar', 'foo.bar@canonical.com')
142
        '"Foo \\[Baz\\] Bar" <foo.bar@canonical.com>'
8137.4.5 by Bjorn Tillenius
Make sure format_address doesn't fold the display names.
143
144
    Really long names doesn't get folded, since we're not constructing
145
    an e-mail header here.
146
147
        >>> formatted_address = format_address(
148
        ...     'a '*100, 'long.name@example.com')
149
        >>> '\n' in formatted_address
150
        False
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
151
    """
152
    if not name:
3424.1.1 by Guilherme Salgado
Fix https://launchpad.net/bugs/28679 (Need email notifications when a person is approved/denied as a member of a team)
153
        return str(address)
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
154
    name = str(Header(name))
8137.4.5 by Bjorn Tillenius
Make sure format_address doesn't fold the display names.
155
    # Using Header to encode the name has the side-effect that long
156
    # names are folded, so let's unfold it again.
157
    name = ''.join(name.splitlines())
2949.2.6 by Bjorn Tillenius
ticket notification should be sent from ticketNN@support.launchpad.net.
158
    return str(formataddr((name, address)))
159
2821.2.1 by Brad Bollenbach
X-Malone-Bug, director's cut
160
7675.624.51 by Tim Penhey
Test passes now.
161
def format_address_for_person(person):
162
    """Helper function to call format_address for a person."""
12001.3.25 by j.c.sackett
Fixed tests.
163
    email_address = removeSecurityProxy(person.preferredemail).email
164
    return format_address(person.displayname, email_address)
7675.624.51 by Tim Penhey
Test passes now.
165
166
14022.2.1 by William Grant
sendmail no longer depends on ZopelessTransactionManager. It uses a module global instead of a class member, and ZTM sets that global.
167
_immediate_mail_delivery = False
168
169
170
def set_immediate_mail_delivery(enabled):
171
    """Enable or disable immediate mail delivery.
172
173
    Mail is by default queued until the transaction is committed. But if
174
    a script requires that mail violate transactions, immediate mail
175
    delivery can be enabled.
176
    """
177
    global _immediate_mail_delivery
178
    _immediate_mail_delivery = enabled
179
180
7763.1.4 by Curtis Hovey
Factored out no_precedence_sendmail, extended simple_sendmail.
181
def simple_sendmail(from_addr, to_addrs, subject, body, headers=None,
182
                    bulk=True):
2821.2.1 by Brad Bollenbach
X-Malone-Bug, director's cut
183
    """Send an email from from_addr to to_addrs with the subject and body
2905.1.1 by Bjorn Tillenius
add simple_sendmail_from_person.
184
    provided. to_addrs can be a list, tuple, or ASCII string.
2821.2.1 by Brad Bollenbach
X-Malone-Bug, director's cut
185
3935.2.2 by Barry Warsaw
Added `mailer` attribute to `mail:mboxMailer` directive so that the message
186
    Arbitrary headers can be set using the headers parameter. If the value for
187
    a given key in the headers dict is a list or tuple, the header will be
7763.1.4 by Curtis Hovey
Factored out no_precedence_sendmail, extended simple_sendmail.
188
    added to the message once for each value in the list.
189
190
    Note however that the `Precedence` header will be set to `bulk` by
191
    default, overriding any `Precedence` header in `headers`.
2821.2.1 by Brad Bollenbach
X-Malone-Bug, director's cut
192
3935.2.6 by Barry Warsaw
Respond to BjornT review comments:
193
    Returns the `Message-Id`.
2821.2.1 by Brad Bollenbach
X-Malone-Bug, director's cut
194
    """
8805.4.1 by Aaron Bentley
Make bulk an ivar on MailController, not a parameter to MC.send.
195
    ctrl = MailController(from_addr, to_addrs, subject, body, headers,
196
                          bulk=bulk)
197
    return ctrl.send()
7763.1.1 by Curtis Hovey
Added no_precedence_send_mail to allow applications to send non-bulk email.
198
199
7325.7.1 by Aaron Bentley
Refactor into MailController
200
class MailController(object):
201
    """Message generation interface closer to peoples' mental model."""
202
8771.4.2 by Aaron Bentley
MailController can specify to address directly.
203
    def __init__(self, from_addr, to_addrs, subject, body, headers=None,
8771.5.7 by Aaron Bentley
Merge devel into synthetic-to.
204
                 envelope_to=None, bulk=True):
7325.7.1 by Aaron Bentley
Refactor into MailController
205
        self.from_addr = from_addr
206
        if zisinstance(to_addrs, basestring):
207
            to_addrs = [to_addrs]
208
        self.to_addrs = to_addrs
8771.4.5 by Aaron Bentley
Rename real_to to envelope_to.
209
        self.envelope_to = envelope_to
7325.7.1 by Aaron Bentley
Refactor into MailController
210
        self.subject = subject
211
        self.body = body
212
        if headers is None:
213
            headers = {}
214
        self.headers = headers
8805.4.1 by Aaron Bentley
Make bulk an ivar on MailController, not a parameter to MC.send.
215
        self.bulk = bulk
7325.7.1 by Aaron Bentley
Refactor into MailController
216
        self.attachments = []
217
7325.7.3 by Aaron Bentley
Fix attachment handling
218
    def addAttachment(self, content, content_type='application/octet-stream',
14100.3.1 by Ian Booth
utf-8 encode diff attachments for branch and code review emails
219
                      inline=False, filename=None, charset=None):
7325.7.3 by Aaron Bentley
Fix attachment handling
220
        attachment = Message()
14100.3.6 by Ian Booth
Improve Content Type header generation
221
        if charset and isinstance(content, unicode):
222
            content = content.encode(charset)
223
        attachment.add_header('Content-Type', content_type)
7325.7.3 by Aaron Bentley
Fix attachment handling
224
        if inline:
225
            disposition = 'inline'
226
        else:
227
            disposition = 'attachment'
228
        disposition_kwargs = {}
229
        if filename is not None:
230
            disposition_kwargs['filename'] = filename
231
        attachment.add_header(
232
            'Content-Disposition', disposition, **disposition_kwargs)
14100.3.1 by Ian Booth
utf-8 encode diff attachments for branch and code review emails
233
        attachment.set_payload(content, charset)
14100.3.4 by Ian Booth
Ensure set_payload is invoked to confirm with older python 2.6 expectations
234
        self.encodeOptimally(attachment)
7325.7.1 by Aaron Bentley
Refactor into MailController
235
        self.attachments.append(attachment)
236
8918.2.2 by Aaron Bentley
Tastefully encode non-ascii attachments.
237
    @staticmethod
8918.2.3 by Aaron Bentley
Ensure line-endings are quoted when exact transfer is demanded.
238
    def encodeOptimally(part, exact=True):
239
        """Encode a message part as needed.
240
241
        If the part is more than 10% high-bit characters, it will be encoded
242
        using base64 encoding.  If the contents are 7-bit and exact is False,
243
        the part will not be encoded.  Otherwise, the message will be encoded
244
        as quoted-printable.
245
246
        If quoted-printable encoding is used, exact will cause all line-ending
247
        characters to be quoted.
248
249
        :param part: The message part to encode.
250
        :param exact: If True, the encoding will ensure newlines are not
251
            mangled.  If False, 7-bit attachments will not be encoded.
252
        """
14100.3.1 by Ian Booth
utf-8 encode diff attachments for branch and code review emails
253
        # If encoding has already been done by virtue of a charset being
254
        # previously specified, then do nothing.
14100.3.2 by Ian Booth
Lint
255
        if 'Content-Transfer-Encoding' in part:
14100.3.1 by Ian Booth
utf-8 encode diff attachments for branch and code review emails
256
            return
8918.2.2 by Aaron Bentley
Tastefully encode non-ascii attachments.
257
        orig_payload = part.get_payload()
8918.2.3 by Aaron Bentley
Ensure line-endings are quoted when exact transfer is demanded.
258
        if not exact and is_ascii_only(orig_payload):
259
            return
8918.2.2 by Aaron Bentley
Tastefully encode non-ascii attachments.
260
        # Payloads which are completely ascii need no encoding.
8918.2.3 by Aaron Bentley
Ensure line-endings are quoted when exact transfer is demanded.
261
        quopri_bytes = b2a_qp(orig_payload, istext=not exact)
8918.2.2 by Aaron Bentley
Tastefully encode non-ascii attachments.
262
        # If 10% of characters need to be encoded, len is 1.2 times
263
        # the original len.  If more than 10% need encoding, the result
264
        # is unlikely to be readable.
265
        if len(quopri_bytes) < len(orig_payload) * 1.2:
266
            part.set_payload(quopri_bytes)
267
            part['Content-Transfer-Encoding'] = 'quoted-printable'
268
        else:
269
            encode_base64(part)
270
7325.7.1 by Aaron Bentley
Refactor into MailController
271
    def makeMessage(self):
7623.1.7 by Paul Hummer
Fixed tests from failed ec2testy run
272
        # It's the caller's responsibility to either encode the address fields
273
        # to ASCII strings or pass in Unicode strings.
7675.229.1 by Tim Penhey
Make the email headers for to and from to be real long, to pass the paranoid checks (for new line characters in them).
274
275
        # Using the maxlinelen for the Headers as we have paranoid checks to
276
        # make sure that we have no carriage returns in the to or from email
277
        # addresses.  We use nice email addresses like 'Launchpad Community
278
        # Help Rotation team <long.email.address+devnull@example.com>' that
279
        # get broken over two lines in the header.  RFC 5322 specified that
280
        # the lines MUST be no more than 998, so we use that as our maximum.
281
        from_addr = Header(self.from_addr, maxlinelen=998).encode()
282
        to_addrs = [Header(address, maxlinelen=998).encode()
7623.1.6 by Paul Hummer
Fixed the implementation of encoding outbound mail
283
            for address in list(self.to_addrs)]
7623.1.2 by Paul Hummer
Fixed a small encoding bug with notifications
284
7623.1.7 by Paul Hummer
Fixed tests from failed ec2testy run
285
        for address in [from_addr] + to_addrs:
7325.7.1 by Aaron Bentley
Refactor into MailController
286
            if not isinstance(address, str) or not is_ascii_only(address):
287
                raise AssertionError(
288
                    'Expected an ASCII str object, got: %r' % address)
289
290
        do_paranoid_email_content_validation(
7623.1.7 by Paul Hummer
Fixed tests from failed ec2testy run
291
            from_addr=from_addr, to_addrs=to_addrs,
7325.7.1 by Aaron Bentley
Refactor into MailController
292
            subject=self.subject, body=self.body)
293
        if len(self.attachments) == 0:
294
            msg = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
295
        else:
7325.7.3 by Aaron Bentley
Fix attachment handling
296
            msg = MIMEMultipart()
8089.3.1 by Tim Penhey
Make unicode emails work when we have an attachment.
297
            body_part = MIMEText(self.body.encode('utf-8'), 'plain', 'utf-8')
7325.7.1 by Aaron Bentley
Refactor into MailController
298
            msg.attach(body_part)
7325.7.3 by Aaron Bentley
Fix attachment handling
299
            for attachment in self.attachments:
300
                msg.attach(attachment)
7325.7.1 by Aaron Bentley
Refactor into MailController
301
302
        # The header_body_values may be a list or tuple of values, so we will
303
        # add a header once for each value provided for that header.
304
        # (X-Launchpad-Bug, for example, may often be set more than once for a
305
        # bugmail.)
306
        for header, header_body_values in self.headers.items():
307
            if not zisinstance(header_body_values, (list, tuple)):
308
                header_body_values = [header_body_values]
309
            for header_body_value in header_body_values:
310
                msg[header] = header_body_value
7623.1.7 by Paul Hummer
Fixed tests from failed ec2testy run
311
        msg['To'] = ','.join(to_addrs)
312
        msg['From'] = from_addr
7325.7.1 by Aaron Bentley
Refactor into MailController
313
        msg['Subject'] = self.subject
314
        return msg
315
7763.1.1 by Curtis Hovey
Added no_precedence_send_mail to allow applications to send non-bulk email.
316
    def send(self, bulk=True):
8771.5.7 by Aaron Bentley
Merge devel into synthetic-to.
317
        return sendmail(self.makeMessage(), self.envelope_to, bulk=self.bulk)
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
318
2699 by Canonical.com Patch Queue Manager
[trivial] fix MAIL FROM to be the standard bounce address. fixes
319
5925.1.1 by Curtis Hovey
Added a mechanism to select the correct conf to load. The switch
320
def simple_sendmail_from_person(
321
    person, to_addrs, subject, body, headers=None):
2905.1.1 by Bjorn Tillenius
add simple_sendmail_from_person.
322
    """Sends a mail using the given person as the From address.
323
324
    It works just like simple_sendmail, excepts that it ensures that the
325
    From header is properly encoded.
326
    """
6596.1.1 by Guilherme Salgado
Fix https://launchpad.net/bugs/241332
327
    from zope.security.proxy import removeSecurityProxy
6596.1.3 by Guilherme Salgado
Some small changes requested by Gavin.
328
    # Bypass zope's security because IEmailAddress.email is not public.
6596.1.1 by Guilherme Salgado
Fix https://launchpad.net/bugs/241332
329
    naked_email = removeSecurityProxy(person.preferredemail)
330
    from_addr = format_address(person.displayname, naked_email.email)
5925.1.1 by Curtis Hovey
Added a mechanism to select the correct conf to load. The switch
331
    return simple_sendmail(
332
        from_addr, to_addrs, subject, body, headers=headers)
2905.1.1 by Bjorn Tillenius
add simple_sendmail_from_person.
333
334
8137.4.2 by Bjorn Tillenius
Unfold the To and CC headers in sendmail().
335
def get_addresses_from_header(email_header):
336
    r"""Get the e-mail addresses specificed in an e-mail header.
337
338
        >>> get_addresses_from_header('one@example.com')
339
        ['one@example.com']
340
        >>> get_addresses_from_header('one@example.com, two@example.com')
341
        ['one@example.com', 'two@example.com']
342
        >>> get_addresses_from_header('One\n <one@example.com>')
343
        ['One <one@example.com>']
344
        >>> get_addresses_from_header('One\r\n <one@example.com>')
345
        ['One <one@example.com>']
8182.1.1 by Bjorn Tillenius
Deal with names that contain commas.
346
        >>> get_addresses_from_header(
347
        ...     '"One, A" <one.a@example.com>,\n'
348
        ...     ' "Two, B" <two.b@example.com>')
349
        ['"One, A" <one.a@example.com>', '"Two, B" <two.b@example.com>']
350
8137.4.2 by Bjorn Tillenius
Unfold the To and CC headers in sendmail().
351
    """
8182.1.1 by Bjorn Tillenius
Deal with names that contain commas.
352
    return [
353
        formataddr((name, address))
354
        for name, address in getaddresses([email_header])]
8137.4.2 by Bjorn Tillenius
Unfold the To and CC headers in sendmail().
355
14027.3.1 by Jeroen Vermeulen
Fix lots of lint in recently-changed files.
356
8970.5.1 by Aaron Bentley
Ensure no codereviewcomment is created if subject missing.
357
def validate_message(message):
8970.5.4 by Aaron Bentley
Update from review.
358
    """Validate that the supplied message is suitable for sending."""
14027.3.1 by Jeroen Vermeulen
Fix lots of lint in recently-changed files.
359
    assert isinstance(message, Message), "Not an email.Message.Message"
360
    assert 'to' in message and bool(message['to']), "No To: header"
361
    assert 'from' in message and bool(message['from']), "No From: header"
362
    assert 'subject' in message and bool(message['subject']), (
363
            "No Subject: header")
364
8137.4.2 by Bjorn Tillenius
Unfold the To and CC headers in sendmail().
365
7703.5.1 by Francis J. Lacoste
Make bulk header optional.
366
def sendmail(message, to_addrs=None, bulk=True):
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
367
    """Send an email.Message.Message
368
369
    If you just need to send dumb ASCII or Unicode, simple_sendmail
370
    will be easier for you. Sending attachments or multipart messages
371
    will need to use this method.
372
373
    From:, To: and Subject: headers should already be set.
2699 by Canonical.com Patch Queue Manager
[trivial] fix MAIL FROM to be the standard bounce address. fixes
374
    Message-Id:, Date:, and Reply-To: headers will be set if they are
2380 by Canonical.com Patch Queue Manager
[r=kiko] ignore emails with an empty Return-Path header.
375
    not already. Errors-To: and Return-Path: headers will always be set.
376
    The more we look valid, the less we look like spam.
377
3287.1.1 by Bjorn Tillenius
add possibility to send mail to other addresses than those found in To: and CC: headers.
378
    If to_addrs is None, the message will be sent to all the addresses
379
    specified in the To: and CC: headers.
380
6061.16.1 by Curtis Hovey
Removed deprecation warning supression code. Updated ZCML, tests, and code to use
381
    Uses zope.sendmail.interfaces.IMailer, so you can subscribe to
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
382
    IMailSentEvent or IMailErrorEvent to record status.
383
13932.1.7 by mbp at canonical
Review tweaks and documentation
384
    This function looks at the `config` singleton for configuration as to
385
    where to send the mail; in particular for whether this code is running in
386
    zopeless mode, and for a `sendmail_to_stdout` attribute for testing.
387
7703.5.1 by Francis J. Lacoste
Make bulk header optional.
388
    :param bulk: By default, a Precedence: bulk header is added to the
389
        message. Pass False to disable this.
390
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
391
    Returns the Message-Id
392
    """
8970.5.1 by Aaron Bentley
Ensure no codereviewcomment is created if subject missing.
393
    validate_message(message)
3287.1.1 by Bjorn Tillenius
add possibility to send mail to other addresses than those found in To: and CC: headers.
394
    if to_addrs is None:
8137.4.2 by Bjorn Tillenius
Unfold the To and CC headers in sendmail().
395
        to_addrs = get_addresses_from_header(message['to'])
3287.1.1 by Bjorn Tillenius
add possibility to send mail to other addresses than those found in To: and CC: headers.
396
        if message['cc']:
8137.4.2 by Bjorn Tillenius
Unfold the To and CC headers in sendmail().
397
            to_addrs = to_addrs + get_addresses_from_header(message['cc'])
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
398
8805.4.3 by Aaron Bentley
Move envelope_to validation to correct place.
399
    do_paranoid_envelope_to_validation(to_addrs)
400
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
401
    # Add a Message-Id: header if it isn't already there
402
    if 'message-id' not in message:
6821.4.3 by Aaron Bentley
Ensure all branch merge proposal emails include an msg_id
403
        message['Message-Id'] = get_msgid()
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
404
405
    # Add a Date: header if it isn't already there
406
    if 'date' not in message:
407
        message['Date'] = formatdate()
408
409
    # Add a Reply-To: header if it isn't already there
410
    if 'reply-to' not in message:
411
        message['Reply-To'] = message['from']
412
3287.1.4 by Bjorn Tillenius
add loop detection.
413
    # Add a Sender: header to show that we were the one sending the
414
    # email.
415
    if "Sender" not in message:
6041.1.1 by Curtis Hovey
Migrated the canonical section.
416
        message["Sender"] = config.canonical.bounce_address
3287.1.4 by Bjorn Tillenius
add loop detection.
417
1697 by Canonical.com Patch Queue Manager
Zopeless email [r=jamesh]
418
    # Add an Errors-To: header for bounce handling
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
419
    del message['Errors-To']
6041.1.1 by Curtis Hovey
Migrated the canonical section.
420
    message['Errors-To'] = config.canonical.bounce_address
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
421
2380 by Canonical.com Patch Queue Manager
[r=kiko] ignore emails with an empty Return-Path header.
422
    # Add a Return-Path: header for bounce handling as well. Normally
423
    # this is added by the SMTP mailer using the From: header. But we
424
    # want it to be bounce_address instead.
425
    if 'return-path' not in message:
6041.1.1 by Curtis Hovey
Migrated the canonical section.
426
        message['Return-Path'] = config.canonical.bounce_address
2380 by Canonical.com Patch Queue Manager
[r=kiko] ignore emails with an empty Return-Path header.
427
7703.5.1 by Francis J. Lacoste
Make bulk header optional.
428
    if bulk:
429
        # Add Precedence header to prevent automatic reply programs
430
        # (e.g. vacation) from trying to respond to our messages.
431
        del message['Precedence']
432
        message['Precedence'] = 'bulk'
3935.2.4 by Barry Warsaw
A fix and a doctest for Bug 90118. Launchpad bug emails need a Precedence:
433
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
434
    # Add an X-Generated-By header for easy whitelisting
435
    del message['X-Generated-By']
7061.3.5 by Gary Poster
change code per review by Edwin: header now generated in more RFC-compliant way, secret header now uses hash for more security and value.
436
    message['X-Generated-By'] = 'Launchpad (canonical.com)'
437
    message.set_param('Revision', str(versioninfo.revno), 'X-Generated-By')
438
    message.set_param('Instance', config.name, 'X-Generated-By')
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
439
7061.3.5 by Gary Poster
change code per review by Edwin: header now generated in more RFC-compliant way, secret header now uses hash for more security and value.
440
    # Add a shared secret header for pre-approval with Mailman. This approach
441
    # helps security, but still exposes us to a replay attack; we consider the
442
    # risk low.
7061.3.7 by Gary Poster
incorporate changes from Barry and Edwin: most notably, header changed to X-Launchpad-Hash
443
    del message['X-Launchpad-Hash']
10293.1.1 by Barry Warsaw, Max Bowsher
Switch from using sha and md5 to hashlib. Also use hashlib.sha256 instead of
444
    hash = hashlib.sha1(config.mailman.shared_secret)
7061.3.5 by Gary Poster
change code per review by Edwin: header now generated in more RFC-compliant way, secret header now uses hash for more security and value.
445
    hash.update(str(message['message-id']))
7061.3.7 by Gary Poster
incorporate changes from Barry and Edwin: most notably, header changed to X-Launchpad-Hash
446
    message['X-Launchpad-Hash'] = hash.hexdigest()
7076.4.1 by Barry Warsaw
Add a shared secret for pre-approved posting between Launchpad and Mailman.
447
1697 by Canonical.com Patch Queue Manager
Zopeless email [r=jamesh]
448
    raw_message = message.as_string()
11502.1.2 by Robert Collins
Record email sending in the request timeline.
449
    message_detail = message['Subject']
14339.2.7 by mbp at canonical
Try to cope if sendmail is called with a Message containing Header objects
450
    if not isinstance(message_detail, basestring):
451
        # Might be a Header object; can be squashed.
452
        message_detail = unicode(message_detail)
14022.2.1 by William Grant
sendmail no longer depends on ZopelessTransactionManager. It uses a module global instead of a class member, and ZTM sets that global.
453
    if _immediate_mail_delivery:
14022.2.2 by William Grant
comments.
454
        # Immediate email delivery is not unit tested, and won't be.
455
        # The immediate-specific stuff is pretty simple though so this
7675.657.1 by Bjorn Tillenius
Revert sendmail.py patch from db-devel r9205.
456
        # should be fine.
14022.2.2 by William Grant
comments.
457
        # TODO: Store a timeline action for immediate mail.
7675.657.1 by Bjorn Tillenius
Revert sendmail.py patch from db-devel r9205.
458
11728.3.6 by Robert Collins
Convert another hard coded 'if testrunner' check, and factor out the duplication.
459
        if config.isTestRunner():
7675.657.1 by Bjorn Tillenius
Revert sendmail.py patch from db-devel r9205.
460
            # when running in the testing environment, store emails
461
            TestMailer().send(
462
                config.canonical.bounce_address, to_addrs, raw_message)
13932.1.7 by mbp at canonical
Review tweaks and documentation
463
        elif getattr(config, 'sendmail_to_stdout', False):
13932.1.6 by mbp at canonical
process-one-mail arranges for sent mail to be printed to stdout.
464
            # For debugging, from process-one-mail, just print it.
465
            sys.stdout.write(raw_message)
7675.657.1 by Bjorn Tillenius
Revert sendmail.py patch from db-devel r9205.
466
        else:
14022.2.3 by William Grant
Rename config.zopeless to config.immediate_mail.
467
            if config.immediate_mail.send_email:
7675.657.1 by Bjorn Tillenius
Revert sendmail.py patch from db-devel r9205.
468
                # Note that we simply throw away dud recipients. This is fine,
469
                # as it emulates the Z3 API which doesn't report this either
470
                # (because actual delivery is done later).
471
                smtp = SMTP(
14022.2.3 by William Grant
Rename config.zopeless to config.immediate_mail.
472
                    config.immediate_mail.smtp_host,
473
                    config.immediate_mail.smtp_port)
7675.657.1 by Bjorn Tillenius
Revert sendmail.py patch from db-devel r9205.
474
475
                # The "MAIL FROM" is set to the bounce address, to behave in a
476
                # way similar to mailing list software.
477
                smtp.sendmail(
478
                    config.canonical.bounce_address, to_addrs, raw_message)
479
                smtp.quit()
4974.5.18 by Francis J. Lacoste
Improve comments.
480
        # Strip the angle brackets to the return a Message-Id consistent with
481
        # raw_sendmail (which doesn't include them).
4974.5.3 by Francis J. Lacoste
Make sendmail() returns a similar value whether running under zopeless or not.
482
        return message['message-id'][1:-1]
1697 by Canonical.com Patch Queue Manager
Zopeless email [r=jamesh]
483
    else:
2699 by Canonical.com Patch Queue Manager
[trivial] fix MAIL FROM to be the standard bounce address. fixes
484
        # The "MAIL FROM" is set to the bounce address, to behave in a way
485
        # similar to mailing list software.
6041.1.1 by Curtis Hovey
Migrated the canonical section.
486
        return raw_sendmail(
11502.1.2 by Robert Collins
Record email sending in the request timeline.
487
            config.canonical.bounce_address,
488
            to_addrs,
489
            raw_message,
490
            message_detail)
2699 by Canonical.com Patch Queue Manager
[trivial] fix MAIL FROM to be the standard bounce address. fixes
491
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
492
6821.4.3 by Aaron Bentley
Ensure all branch merge proposal emails include an msg_id
493
def get_msgid():
6821.4.7 by Aaron Bentley
Remove @canonical from message ids
494
    return make_msgid('launchpad')
6821.4.3 by Aaron Bentley
Ensure all branch merge proposal emails include an msg_id
495
496
11502.1.2 by Robert Collins
Record email sending in the request timeline.
497
def raw_sendmail(from_addr, to_addrs, raw_message, message_detail):
2699 by Canonical.com Patch Queue Manager
[trivial] fix MAIL FROM to be the standard bounce address. fixes
498
    """Send a raw RFC8222 email message.
499
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
500
    All headers and encoding should already be done, as the message is
501
    spooled out verbatim to the delivery agent.
502
503
    You should not need to call this method directly, although it may be
504
    necessary to pass on signed or encrypted messages.
505
1697 by Canonical.com Patch Queue Manager
Zopeless email [r=jamesh]
506
    Returns the message-id.
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
507
14339.2.1 by mbp at canonical
Add test for timeline created by raw_sendmail (see bug 885972)
508
    :param message_detail: String of detail about the message
14339.2.10 by mbp at canonical
Review style/comment tweaks
509
        to be recorded to help with debugging, eg the message subject.
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
510
    """
511
    assert not isinstance(to_addrs, basestring), 'to_addrs must be a sequence'
512
    assert isinstance(raw_message, str), 'Not a plain string'
513
    assert raw_message.decode('ascii'), 'Not ASCII - badly encoded message'
514
    mailer = zapi.getUtility(IMailDelivery, 'Mail')
11502.1.2 by Robert Collins
Record email sending in the request timeline.
515
    request = get_current_browser_request()
516
    timeline = get_request_timeline(request)
517
    action = timeline.start("sendmail", message_detail)
518
    try:
519
        return mailer.send(from_addr, to_addrs, raw_message)
520
    finally:
521
        action.finish()