From d0046196d84411ebf059453c0b519e9e6e5e9074 Mon Sep 17 00:00:00 2001 From: Pablo Hoffman Date: Sun, 11 Jan 2009 23:04:50 +0000 Subject: [PATCH] ported MailSender class to use twisted non-blocking IO --HG-- extra : convert_revision : svn%3Ab85faa78-f9eb-468e-a121-7cced6da292c%40708 --- scrapy/trunk/docs/ref/email.rst | 26 ++++++------- scrapy/trunk/scrapy/mail/__init__.py | 55 ++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/scrapy/trunk/docs/ref/email.rst b/scrapy/trunk/docs/ref/email.rst index cc56c4aee..deca9a384 100644 --- a/scrapy/trunk/docs/ref/email.rst +++ b/scrapy/trunk/docs/ref/email.rst @@ -7,13 +7,14 @@ Sending email .. module:: scrapy.mail :synopsis: Helpers to easily send e-mail. -Although Python makes sending e-mail relatively easy via the `smtplib library`_, -Scrapy provides a couple of light wrappers over it, to make sending e-mail -extra quick. +Although Python makes sending e-mail relatively easy via the `smtplib`_ +library, Scrapy provides its own class for sending emails which is very easy to +use and it's implemented using `Twisted non-blocking IO`_, to avoid affecting +the crawling performance. -The code lives in a single module: ``scrapy.mail``. +.. _smtplib: http://docs.python.org/library/smtplib.html -.. _smtplib library: http://docs.python.org/library/smtplib.html +It also has built-in support for sending attachments. Quick example ============= @@ -25,14 +26,11 @@ Here's a quick example of how to send an email (without attachments):: mailer = MailSender() mailer.send(to=["someone@example.com"], "Some subject", "Some body", cc=["another@example.com"]) -MailSender class -================ - -MailSender is the class used to send emails from Scrapy. It's -currently only a wrapper over the (IO blocking) `smtplib`_ -library but it's gonna be ported to Twisted soon. +MailSender class reference +========================== -.. _smtplib: http://docs.python.org/library/smtplib.html +MailSender is the preferred class to use for sending emails from Scrapy, as it +uses `Twisted non-blocking IO`_, like the rest of the framework. .. class:: scrapy.mail.MailSender(smtphost, mailfrom) @@ -42,7 +40,7 @@ library but it's gonna be ported to Twisted soon. ``mailfrom`` is a string with the email address to use for sending messages (in the ``From:`` header). If omitted, :setting:`MAIL_FROM` will be used. -.. method:: send(to, subject, body, cc=None, attachs=None) +.. method:: send(to, subject, body, cc=None, attachs=()) Send mail to the given recipients @@ -59,3 +57,5 @@ library but it's gonna be ported to Twisted soon. ``mimetype`` is the mimetype of the attachment ``file_object`` is a readable file object + +.. _Twisted non-blocking IO: http://twistedmatrix.com/projects/core/documentation/howto/async.html diff --git a/scrapy/trunk/scrapy/mail/__init__.py b/scrapy/trunk/scrapy/mail/__init__.py index 2220375df..9745ec3bc 100644 --- a/scrapy/trunk/scrapy/mail/__init__.py +++ b/scrapy/trunk/scrapy/mail/__init__.py @@ -3,14 +3,17 @@ Mail sending helpers See documentation in docs/ref/email.rst """ -import smtplib - +from cStringIO import StringIO from email.MIMEMultipart import MIMEMultipart +from email.MIMENonMultipart import MIMENonMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate from email import Encoders +from twisted.internet import defer, reactor +from twisted.mail.smtp import SMTPSenderFactory + from scrapy import log from scrapy.core.exceptions import NotConfigured from scrapy.conf import settings @@ -24,8 +27,11 @@ class MailSender(object): if not self.smtphost or not self.mailfrom: raise NotConfigured("MAIL_HOST and MAIL_FROM settings are required") - def send(self, to, subject, body, cc=None, attachs=None): - msg = MIMEMultipart() + def send(self, to, subject, body, cc=None, attachs=()): + if attachs: + msg = MIMEMultipart() + else: + msg = MIMENonMultipart('text', 'plain') msg['From'] = self.mailfrom msg['To'] = COMMASPACE.join(to) msg['Date'] = formatdate(localtime=True) @@ -35,17 +41,36 @@ class MailSender(object): rcpts.extend(cc) msg['Cc'] = COMMASPACE.join(cc) - msg.attach(MIMEText(body)) + if attachs: + msg.attach(MIMEText(body)) + for attach_name, mimetype, f in attachs: + part = MIMEBase(*mimetype.split('/')) + part.set_payload(f.read()) + Encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="%s"' % attach_name) + msg.attach(part) + else: + msg.set_payload(body) + + dfd = self._sendmail(self.smtphost, self.mailfrom, rcpts, msg.as_string()) + dfd.addCallbacks(self._sent_ok, self._sent_failed, + callbackArgs=[to, cc, subject, len(attachs)], + errbackArgs=[to, cc, subject, len(attachs)]) + + def _sent_ok(self, result, to, cc, subject, nattachs): + log.msg('Mail sent OK: To=%s Cc=%s Subject="%s" Attachs=%d' % (to, cc, subject, nattachs)) - for attach_name, mimetype, f in (attachs or []): - part = MIMEBase(*mimetype.split('/')) - part.set_payload(f.read()) - Encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="%s"' % attach_name) - msg.attach(part) + def _sent_failed(self, failure, to, cc, subject, nattachs): + errstr = str(failure.value) + log.msg('Unable to send mail: To=%s Cc=%s Subject="%s" Attachs=%d - %s' % (to, cc, subject, nattachs, errstr), level=log.ERROR) - smtp = smtplib.SMTP(self.smtphost) - smtp.sendmail(self.mailfrom, rcpts, msg.as_string()) - log.msg('Mail sent: To=%s Cc=%s Subject="%s"' % (to, cc, subject)) - smtp.close() + def _sendmail(self, smtphost, from_addr, to_addrs, msg, port=25): + """ This is based on twisted.mail.smtp.sendmail except that it + instantiates a quiet (noisy=False) SMTPSenderFactory """ + msg = StringIO(msg) + d = defer.Deferred() + factory = SMTPSenderFactory(from_addr, to_addrs, msg, d) + factory.noisy = False + reactor.connectTCP(smtphost, port, factory) + return d -- GitLab