You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
4.9 KiB

4 years ago
  1. """
  2. Mail sending helpers
  3. See documentation in docs/topics/email.rst
  4. """
  5. import logging
  6. try:
  7. from cStringIO import StringIO as BytesIO
  8. except ImportError:
  9. from io import BytesIO
  10. import six
  11. from email.utils import COMMASPACE, formatdate
  12. from six.moves.email_mime_multipart import MIMEMultipart
  13. from six.moves.email_mime_text import MIMEText
  14. from six.moves.email_mime_base import MIMEBase
  15. if six.PY2:
  16. from email.MIMENonMultipart import MIMENonMultipart
  17. from email import Encoders
  18. else:
  19. from email.mime.nonmultipart import MIMENonMultipart
  20. from email import encoders as Encoders
  21. from twisted.internet import defer, reactor, ssl
  22. from scrapy.utils.misc import arg_to_iter
  23. from scrapy.utils.python import to_bytes
  24. logger = logging.getLogger(__name__)
  25. def _to_bytes_or_none(text):
  26. if text is None:
  27. return None
  28. return to_bytes(text)
  29. class MailSender(object):
  30. def __init__(self, smtphost='localhost', mailfrom='scrapy@localhost',
  31. smtpuser=None, smtppass=None, smtpport=25, smtptls=False, smtpssl=False, debug=False):
  32. self.smtphost = smtphost
  33. self.smtpport = smtpport
  34. self.smtpuser = _to_bytes_or_none(smtpuser)
  35. self.smtppass = _to_bytes_or_none(smtppass)
  36. self.smtptls = smtptls
  37. self.smtpssl = smtpssl
  38. self.mailfrom = mailfrom
  39. self.debug = debug
  40. @classmethod
  41. def from_settings(cls, settings):
  42. return cls(settings['MAIL_HOST'], settings['MAIL_FROM'], settings['MAIL_USER'],
  43. settings['MAIL_PASS'], settings.getint('MAIL_PORT'),
  44. settings.getbool('MAIL_TLS'), settings.getbool('MAIL_SSL'))
  45. def send(self, to, subject, body, cc=None, attachs=(), mimetype='text/plain', charset=None, _callback=None):
  46. if attachs:
  47. msg = MIMEMultipart()
  48. else:
  49. msg = MIMENonMultipart(*mimetype.split('/', 1))
  50. to = list(arg_to_iter(to))
  51. cc = list(arg_to_iter(cc))
  52. msg['From'] = self.mailfrom
  53. msg['To'] = COMMASPACE.join(to)
  54. msg['Date'] = formatdate(localtime=True)
  55. msg['Subject'] = subject
  56. rcpts = to[:]
  57. if cc:
  58. rcpts.extend(cc)
  59. msg['Cc'] = COMMASPACE.join(cc)
  60. if charset:
  61. msg.set_charset(charset)
  62. if attachs:
  63. msg.attach(MIMEText(body, 'plain', charset or 'us-ascii'))
  64. for attach_name, mimetype, f in attachs:
  65. part = MIMEBase(*mimetype.split('/'))
  66. part.set_payload(f.read())
  67. Encoders.encode_base64(part)
  68. part.add_header('Content-Disposition', 'attachment; filename="%s"' \
  69. % attach_name)
  70. msg.attach(part)
  71. else:
  72. msg.set_payload(body)
  73. if _callback:
  74. _callback(to=to, subject=subject, body=body, cc=cc, attach=attachs, msg=msg)
  75. if self.debug:
  76. logger.debug('Debug mail sent OK: To=%(mailto)s Cc=%(mailcc)s '
  77. 'Subject="%(mailsubject)s" Attachs=%(mailattachs)d',
  78. {'mailto': to, 'mailcc': cc, 'mailsubject': subject,
  79. 'mailattachs': len(attachs)})
  80. return
  81. dfd = self._sendmail(rcpts, msg.as_string().encode(charset or 'utf-8'))
  82. dfd.addCallbacks(self._sent_ok, self._sent_failed,
  83. callbackArgs=[to, cc, subject, len(attachs)],
  84. errbackArgs=[to, cc, subject, len(attachs)])
  85. reactor.addSystemEventTrigger('before', 'shutdown', lambda: dfd)
  86. return dfd
  87. def _sent_ok(self, result, to, cc, subject, nattachs):
  88. logger.info('Mail sent OK: To=%(mailto)s Cc=%(mailcc)s '
  89. 'Subject="%(mailsubject)s" Attachs=%(mailattachs)d',
  90. {'mailto': to, 'mailcc': cc, 'mailsubject': subject,
  91. 'mailattachs': nattachs})
  92. def _sent_failed(self, failure, to, cc, subject, nattachs):
  93. errstr = str(failure.value)
  94. logger.error('Unable to send mail: To=%(mailto)s Cc=%(mailcc)s '
  95. 'Subject="%(mailsubject)s" Attachs=%(mailattachs)d'
  96. '- %(mailerr)s',
  97. {'mailto': to, 'mailcc': cc, 'mailsubject': subject,
  98. 'mailattachs': nattachs, 'mailerr': errstr})
  99. def _sendmail(self, to_addrs, msg):
  100. # Import twisted.mail here because it is not available in python3
  101. from twisted.mail.smtp import ESMTPSenderFactory
  102. msg = BytesIO(msg)
  103. d = defer.Deferred()
  104. factory = ESMTPSenderFactory(self.smtpuser, self.smtppass, self.mailfrom, \
  105. to_addrs, msg, d, heloFallback=True, requireAuthentication=False, \
  106. requireTransportSecurity=self.smtptls)
  107. factory.noisy = False
  108. if self.smtpssl:
  109. reactor.connectSSL(self.smtphost, self.smtpport, factory, ssl.ClientContextFactory())
  110. else:
  111. reactor.connectTCP(self.smtphost, self.smtpport, factory)
  112. return d