email.py 7.5KB

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