email.py 8.1KB

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