email.py 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. # -*- coding: utf-8 -*-
  2. import smtplib
  3. import typing
  4. from email.message import Message
  5. from email.mime.multipart import MIMEMultipart
  6. from email.mime.text import MIMEText
  7. from mako.template import Template
  8. from tg.i18n import ugettext as _
  9. from tracim.lib.base import logger
  10. from tracim.lib.utils import get_rq_queue
  11. from tracim.model import User
  12. def send_email_through(
  13. sendmail_callable: typing.Callable[[Message], None],
  14. message: Message,
  15. ) -> None:
  16. """
  17. Send mail encapsulation to send it in async or sync mode.
  18. TODO BS 20170126: A global mail/sender management should be a good
  19. thing. Actually, this method is an fast solution.
  20. :param sendmail_callable: A callable who get message on first parameter
  21. :param message: The message who have to be sent
  22. """
  23. from tracim.config.app_cfg import CFG
  24. cfg = CFG.get_instance()
  25. if cfg.EMAIL_PROCESSING_MODE == CFG.CST.SYNC:
  26. sendmail_callable(message)
  27. elif cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC:
  28. queue = get_rq_queue('mail_sender')
  29. queue.enqueue(sendmail_callable, message)
  30. else:
  31. raise NotImplementedError(
  32. 'Mail sender processing mode {} is not implemented'.format(
  33. cfg.EMAIL_PROCESSING_MODE,
  34. )
  35. )
  36. class SmtpConfiguration(object):
  37. """Container class for SMTP configuration used in Tracim."""
  38. def __init__(self, server: str, port: int, login: str, password: str):
  39. self.server = server
  40. self.port = port
  41. self.login = login
  42. self.password = password
  43. class EmailSender(object):
  44. """
  45. Independent email sender class.
  46. To allow its use in any thread, as an asyncjob_perform() call for
  47. example, it has no dependencies on SQLAlchemy nor tg HTTP request.
  48. """
  49. def __init__(self, config: SmtpConfiguration, really_send_messages):
  50. self._smtp_config = config
  51. self._smtp_connection = None
  52. self._is_active = really_send_messages
  53. def connect(self):
  54. if not self._smtp_connection:
  55. log = 'Connecting from SMTP server {}'
  56. logger.info(self, log.format(self._smtp_config.server))
  57. self._smtp_connection = smtplib.SMTP(
  58. self._smtp_config.server,
  59. self._smtp_config.port
  60. )
  61. self._smtp_connection.ehlo()
  62. if self._smtp_config.login:
  63. try:
  64. starttls_result = self._smtp_connection.starttls()
  65. log = 'SMTP start TLS result: {}'
  66. logger.debug(self, log.format(starttls_result))
  67. except Exception as e:
  68. log = 'SMTP start TLS error: {}'
  69. logger.debug(self, log.format(e.__str__()))
  70. if self._smtp_config.login:
  71. try:
  72. login_res = self._smtp_connection.login(
  73. self._smtp_config.login,
  74. self._smtp_config.password
  75. )
  76. log = 'SMTP login result: {}'
  77. logger.debug(self, log.format(login_res))
  78. except Exception as e:
  79. log = 'SMTP login error: {}'
  80. logger.debug(self, log.format(e.__str__()))
  81. logger.info(self, 'Connection OK')
  82. def disconnect(self):
  83. if self._smtp_connection:
  84. log = 'Disconnecting from SMTP server {}'
  85. logger.info(self, log.format(self._smtp_config.server))
  86. self._smtp_connection.quit()
  87. logger.info(self, 'Connection closed.')
  88. def send_mail(self, message: MIMEMultipart):
  89. if not self._is_active:
  90. log = 'Not sending email to {} (service disabled)'
  91. logger.info(self, log.format(message['To']))
  92. else:
  93. self.connect() # Actually, this connects to SMTP only if required
  94. logger.info(self, 'Sending email to {}'.format(message['To']))
  95. self._smtp_connection.send_message(message)
  96. from tracim.lib.notifications import EmailNotifier
  97. EmailNotifier.log_notification(
  98. action=' SENT',
  99. recipient=message['To'],
  100. subject=message['Subject'],
  101. )
  102. class EmailManager(object):
  103. def __init__(self, smtp_config: SmtpConfiguration, global_config):
  104. self._smtp_config = smtp_config
  105. self._global_config = global_config
  106. def notify_created_account(
  107. self,
  108. user: User,
  109. password: str,
  110. ) -> None:
  111. """
  112. Send created account email to given user.
  113. :param password: choosed password
  114. :param user: user to notify
  115. """
  116. # TODO BS 20160712: Cyclic import
  117. from tracim.lib.notifications import EST
  118. logger.debug(self, 'user: {}'.format(user.user_id))
  119. logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format(
  120. user.email,
  121. ))
  122. async_email_sender = EmailSender(
  123. self._smtp_config,
  124. self._global_config.EMAIL_NOTIFICATION_ACTIVATED
  125. )
  126. subject = \
  127. self._global_config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \
  128. .replace(
  129. EST.WEBSITE_TITLE,
  130. self._global_config.WEBSITE_TITLE.__str__()
  131. )
  132. message = MIMEMultipart('alternative')
  133. message['Subject'] = subject
  134. message['From'] = '{0} <{1}>'.format(
  135. self._global_config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL,
  136. self._global_config.EMAIL_NOTIFICATION_FROM_EMAIL,
  137. )
  138. message['To'] = user.email
  139. text_template_file_path = self._global_config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT # nopep8
  140. html_template_file_path = self._global_config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML # nopep8
  141. body_text = self._render(
  142. mako_template_filepath=text_template_file_path,
  143. context={
  144. 'user': user,
  145. 'password': password,
  146. 'login_url': self._global_config.WEBSITE_BASE_URL,
  147. }
  148. )
  149. body_html = self._render(
  150. mako_template_filepath=html_template_file_path,
  151. context={
  152. 'user': user,
  153. 'password': password,
  154. 'login_url': self._global_config.WEBSITE_BASE_URL,
  155. }
  156. )
  157. part1 = MIMEText(body_text, 'plain', 'utf-8')
  158. part2 = MIMEText(body_html, 'html', 'utf-8')
  159. # Attach parts into message container.
  160. # According to RFC 2046, the last part of a multipart message,
  161. # in this case the HTML message, is best and preferred.
  162. message.attach(part1)
  163. message.attach(part2)
  164. send_email_through(async_email_sender.send_mail, message)
  165. def _render(self, mako_template_filepath: str, context: dict):
  166. """
  167. Render mako template with all needed current variables.
  168. :param mako_template_filepath: file path of mako template
  169. :param context: dict with template context
  170. :return: template rendered string
  171. """
  172. # TODO - D.A. - 2014-11-06 - move this
  173. # Import is here for circular import problem
  174. import tracim.lib.helpers as helpers
  175. from tracim.config.app_cfg import CFG
  176. template = Template(filename=mako_template_filepath)
  177. return template.render(
  178. base_url=self._global_config.WEBSITE_BASE_URL,
  179. _=_,
  180. h=helpers,
  181. CFG=CFG.get_instance(),
  182. **context
  183. )
  184. def get_email_manager():
  185. """
  186. :return: EmailManager instance
  187. """
  188. #  TODO: Find a way to import properly without cyclic import
  189. from tracim.config.app_cfg import CFG
  190. global_config = CFG.get_instance()
  191. smtp_config = SmtpConfiguration(
  192. global_config.EMAIL_NOTIFICATION_SMTP_SERVER,
  193. global_config.EMAIL_NOTIFICATION_SMTP_PORT,
  194. global_config.EMAIL_NOTIFICATION_SMTP_USER,
  195. global_config.EMAIL_NOTIFICATION_SMTP_PASSWORD
  196. )
  197. return EmailManager(global_config=global_config, smtp_config=smtp_config)