~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/services/mail/sendmail.py

  • Committer: Curtis Hovey
  • Date: 2011-08-18 20:56:37 UTC
  • mto: This revision was merged to the branch mainline in revision 13736.
  • Revision ID: curtis.hovey@canonical.com-20110818205637-ae0pf9aexdea2mlb
Cleaned up doctrings and hushed lint.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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).
3
3
 
4
4
"""The One True Way to send mail from the Launchpad application.
21
21
    'get_msgid',
22
22
    'MailController',
23
23
    'sendmail',
24
 
    'set_immediate_mail_delivery',
25
24
    'simple_sendmail',
26
25
    'simple_sendmail_from_person',
27
26
    'validate_message',
43
42
    )
44
43
import hashlib
45
44
from smtplib import SMTP
46
 
import sys
47
45
 
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,
52
 
    removeSecurityProxy,
53
 
    )
 
48
from zope.security.proxy import isinstance as zisinstance
 
49
from zope.security.proxy import removeSecurityProxy
54
50
from zope.sendmail.interfaces import IMailDelivery
55
51
 
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')
68
65
 
69
 
 
70
66
def do_paranoid_email_content_validation(from_addr, to_addrs, subject, body):
71
67
    """Validate various bits of the email.
72
68
 
164
160
    return format_address(person.displayname, email_address)
165
161
 
166
162
 
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
 
 
181
163
def simple_sendmail(from_addr, to_addrs, subject, body, headers=None,
182
164
                    bulk=True):
183
165
    """Send an email from from_addr to to_addrs with the subject and body
216
198
        self.attachments = []
217
199
 
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
224
205
        if inline:
225
206
            disposition = 'inline'
226
207
        else:
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)
236
216
 
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.
252
232
        """
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:
256
 
            return
257
233
        orig_payload = part.get_payload()
258
234
        if not exact and is_ascii_only(orig_payload):
259
235
            return
353
329
        formataddr((name, address))
354
330
        for name, address in getaddresses([email_header])]
355
331
 
356
 
 
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")
364
 
 
 
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']), \
 
338
            'No Subject: header'
365
339
 
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.
383
357
 
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
 
 
388
358
    :param bulk: By default, a Precedence: bulk header is added to the
389
359
        message. Pass False to disable this.
390
360
 
447
417
 
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
 
420
    if isZopeless():
 
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.
458
425
 
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)
466
430
        else:
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).
471
435
                smtp = SMTP(
472
 
                    config.immediate_mail.smtp_host,
473
 
                    config.immediate_mail.smtp_port)
 
436
                    config.zopeless.smtp_host, config.zopeless.smtp_port)
474
437
 
475
438
                # The "MAIL FROM" is set to the bounce address, to behave in a
476
439
                # way similar to mailing list software.
505
468
 
506
469
    Returns the message-id.
507
470
 
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
 
472
        request timeline.
510
473
    """
 
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)
520
484
    finally:
521
485
        action.finish()
 
486
 
 
487
 
 
488
if __name__ == '__main__':
 
489
    from canonical.lp import initZopeless
 
490
    tm = initZopeless()
 
491
    simple_sendmail(
 
492
            'stuart.bishop@canonical.com', ['stuart@stuartbishop.net'],
 
493
            'Testing Zopeless', 'This is the body')
 
494
    tm.uninstall()