mail.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # -*- coding: utf-8 -*-
  2. """
  3. celery.utils.mail
  4. ~~~~~~~~~~~~~~~~~
  5. How task error emails are formatted and sent.
  6. """
  7. from __future__ import absolute_import
  8. import smtplib
  9. import socket
  10. import traceback
  11. import warnings
  12. from email.mime.text import MIMEText
  13. from .functional import maybe_list
  14. try:
  15. from ssl import SSLError
  16. except ImportError: # pragma: no cover
  17. class SSLError(Exception): # noqa
  18. """fallback used when ssl module not compiled."""
  19. __all__ = ['SendmailWarning', 'Message', 'Mailer', 'ErrorMail']
  20. _local_hostname = None
  21. def get_local_hostname():
  22. global _local_hostname
  23. if _local_hostname is None:
  24. _local_hostname = socket.getfqdn()
  25. return _local_hostname
  26. class SendmailWarning(UserWarning):
  27. """Problem happened while sending the email message."""
  28. class Message(object):
  29. def __init__(self, to=None, sender=None, subject=None,
  30. body=None, charset='us-ascii'):
  31. self.to = maybe_list(to)
  32. self.sender = sender
  33. self.subject = subject
  34. self.body = body
  35. self.charset = charset
  36. def __repr__(self):
  37. return '<Email: To:{0.to!r} Subject:{0.subject!r}>'.format(self)
  38. def __str__(self):
  39. msg = MIMEText(self.body, 'plain', self.charset)
  40. msg['Subject'] = self.subject
  41. msg['From'] = self.sender
  42. msg['To'] = ', '.join(self.to)
  43. return msg.as_string()
  44. class Mailer(object):
  45. def __init__(self, host='localhost', port=0, user=None, password=None,
  46. timeout=2, use_ssl=False, use_tls=False):
  47. self.host = host
  48. self.port = port
  49. self.user = user
  50. self.password = password
  51. self.timeout = timeout
  52. self.use_ssl = use_ssl
  53. self.use_tls = use_tls
  54. def send(self, message, fail_silently=False, **kwargs):
  55. try:
  56. self._send(message, **kwargs)
  57. except Exception as exc:
  58. if not fail_silently:
  59. raise
  60. warnings.warn(SendmailWarning(
  61. 'Mail could not be sent: {0!r} {1!r}\n{2!r}'.format(
  62. exc, {'To': ', '.join(message.to),
  63. 'Subject': message.subject},
  64. traceback.format_stack())))
  65. def _send(self, message, **kwargs):
  66. Client = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
  67. client = Client(self.host, self.port, timeout=self.timeout,
  68. local_hostname=get_local_hostname(), **kwargs)
  69. if self.use_tls:
  70. client.ehlo()
  71. client.starttls()
  72. client.ehlo()
  73. if self.user and self.password:
  74. client.login(self.user, self.password)
  75. client.sendmail(message.sender, message.to, str(message))
  76. try:
  77. client.quit()
  78. except SSLError:
  79. client.close()
  80. class ErrorMail(object):
  81. """Defines how and when task error e-mails should be sent.
  82. :param task: The task instance that raised the error.
  83. :attr:`subject` and :attr:`body` are format strings which
  84. are passed a context containing the following keys:
  85. * name
  86. Name of the task.
  87. * id
  88. UUID of the task.
  89. * exc
  90. String representation of the exception.
  91. * args
  92. Positional arguments.
  93. * kwargs
  94. Keyword arguments.
  95. * traceback
  96. String representation of the traceback.
  97. * hostname
  98. Worker nodename.
  99. """
  100. # pep8.py borks on a inline signature separator and
  101. # says "trailing whitespace" ;)
  102. EMAIL_SIGNATURE_SEP = '-- '
  103. #: Format string used to generate error email subjects.
  104. subject = """\
  105. [{hostname}] Error: Task {name} ({id}): {exc!r}
  106. """
  107. #: Format string used to generate error email content.
  108. body = """
  109. Task {{name}} with id {{id}} raised exception:\n{{exc!r}}
  110. Task was called with args: {{args}} kwargs: {{kwargs}}.
  111. The contents of the full traceback was:
  112. {{traceback}}
  113. {EMAIL_SIGNATURE_SEP}
  114. Just to let you know,
  115. py-celery at {{hostname}}.
  116. """.format(EMAIL_SIGNATURE_SEP=EMAIL_SIGNATURE_SEP)
  117. def __init__(self, task, **kwargs):
  118. self.task = task
  119. self.subject = kwargs.get('subject', self.subject)
  120. self.body = kwargs.get('body', self.body)
  121. def should_send(self, context, exc):
  122. """Return true or false depending on if a task error mail
  123. should be sent for this type of error."""
  124. return True
  125. def format_subject(self, context):
  126. return self.subject.strip().format(**context)
  127. def format_body(self, context):
  128. return self.body.strip().format(**context)
  129. def send(self, context, exc, fail_silently=True):
  130. if self.should_send(context, exc):
  131. self.task.app.mail_admins(self.format_subject(context),
  132. self.format_body(context),
  133. fail_silently=fail_silently)