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() |