1
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
1
# Copyright 2009 Canonical Ltd. This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
4
"""The One True Way to send mail from the Launchpad application.
45
44
from smtplib import SMTP
48
46
from lazr.restful.utils import get_current_browser_request
49
47
from zope.app import zapi
50
from zope.security.proxy import (
51
isinstance as zisinstance,
48
from zope.security.proxy import isinstance as zisinstance
49
from zope.security.proxy import removeSecurityProxy
54
50
from zope.sendmail.interfaces import IMailDelivery
56
52
from canonical.config import config
53
from canonical.lp import isZopeless
57
54
from lp.app import versioninfo
58
55
from lp.services.encoding import is_ascii_only
59
56
from lp.services.mail.stub import TestMailer
66
63
Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
67
64
Charset.add_alias('utf8', 'utf-8')
70
66
def do_paranoid_email_content_validation(from_addr, to_addrs, subject, body):
71
67
"""Validate various bits of the email.
164
160
return format_address(person.displayname, email_address)
167
_immediate_mail_delivery = False
170
def set_immediate_mail_delivery(enabled):
171
"""Enable or disable immediate mail delivery.
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.
177
global _immediate_mail_delivery
178
_immediate_mail_delivery = enabled
181
163
def simple_sendmail(from_addr, to_addrs, subject, body, headers=None,
183
165
"""Send an email from from_addr to to_addrs with the subject and body
216
198
self.attachments = []
218
200
def addAttachment(self, content, content_type='application/octet-stream',
219
inline=False, filename=None, charset=None):
201
inline=False, filename=None):
220
202
attachment = Message()
221
if charset and isinstance(content, unicode):
222
content = content.encode(charset)
223
attachment.add_header('Content-Type', content_type)
203
attachment.set_payload(content)
204
attachment['Content-type'] = content_type
225
206
disposition = 'inline'
230
211
disposition_kwargs['filename'] = filename
231
212
attachment.add_header(
232
213
'Content-Disposition', disposition, **disposition_kwargs)
233
attachment.set_payload(content, charset)
234
214
self.encodeOptimally(attachment)
235
215
self.attachments.append(attachment)
250
230
:param exact: If True, the encoding will ensure newlines are not
251
231
mangled. If False, 7-bit attachments will not be encoded.
253
# If encoding has already been done by virtue of a charset being
254
# previously specified, then do nothing.
255
if 'Content-Transfer-Encoding' in part:
257
233
orig_payload = part.get_payload()
258
234
if not exact and is_ascii_only(orig_payload):
353
329
formataddr((name, address))
354
330
for name, address in getaddresses([email_header])]
357
332
def validate_message(message):
358
333
"""Validate that the supplied message is suitable for sending."""
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")
334
assert isinstance(message, Message), 'Not an email.Message.Message'
335
assert 'to' in message and bool(message['to']), 'No To: header'
336
assert 'from' in message and bool(message['from']), 'No From: header'
337
assert 'subject' in message and bool(message['subject']), \
366
340
def sendmail(message, to_addrs=None, bulk=True):
367
341
"""Send an email.Message.Message
381
355
Uses zope.sendmail.interfaces.IMailer, so you can subscribe to
382
356
IMailSentEvent or IMailErrorEvent to record status.
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.
388
358
:param bulk: By default, a Precedence: bulk header is added to the
389
359
message. Pass False to disable this.
448
418
raw_message = message.as_string()
449
419
message_detail = message['Subject']
450
if not isinstance(message_detail, basestring):
451
# Might be a Header object; can be squashed.
452
message_detail = unicode(message_detail)
453
if _immediate_mail_delivery:
454
# Immediate email delivery is not unit tested, and won't be.
455
# The immediate-specific stuff is pretty simple though so this
421
# Zopeless email sending is not unit tested, and won't be.
422
# The zopeless specific stuff is pretty simple though so this
456
423
# should be fine.
457
# TODO: Store a timeline action for immediate mail.
424
# TODO: Store a timeline action for zopeless mail.
459
426
if config.isTestRunner():
460
427
# when running in the testing environment, store emails
461
428
TestMailer().send(
462
429
config.canonical.bounce_address, to_addrs, raw_message)
463
elif getattr(config, 'sendmail_to_stdout', False):
464
# For debugging, from process-one-mail, just print it.
465
sys.stdout.write(raw_message)
467
if config.immediate_mail.send_email:
431
if config.zopeless.send_email:
468
432
# Note that we simply throw away dud recipients. This is fine,
469
433
# as it emulates the Z3 API which doesn't report this either
470
434
# (because actual delivery is done later).
472
config.immediate_mail.smtp_host,
473
config.immediate_mail.smtp_port)
436
config.zopeless.smtp_host, config.zopeless.smtp_port)
475
438
# The "MAIL FROM" is set to the bounce address, to behave in a
476
439
# way similar to mailing list software.
506
469
Returns the message-id.
508
:param message_detail: String of detail about the message
509
to be recorded to help with debugging, eg the message subject.
471
:param message_detail: Information about the message to include in the
474
# Note that raw_sendail has no tests, unit or otherwise.
511
475
assert not isinstance(to_addrs, basestring), 'to_addrs must be a sequence'
512
476
assert isinstance(raw_message, str), 'Not a plain string'
513
477
assert raw_message.decode('ascii'), 'Not ASCII - badly encoded message'
519
483
return mailer.send(from_addr, to_addrs, raw_message)
488
if __name__ == '__main__':
489
from canonical.lp import initZopeless
492
'stuart.bishop@canonical.com', ['stuart@stuartbishop.net'],
493
'Testing Zopeless', 'This is the body')