| Total Complexity | 45 | 
| Total Lines | 727 | 
| Duplicated Lines | 18.57 % | 
| Changes | 0 | ||
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like notifier often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | # -*- coding: utf-8 -*-  | 
            ||
| 2 | import datetime  | 
            ||
| 3 | import typing  | 
            ||
| 4 | from email.mime.multipart import MIMEMultipart  | 
            ||
| 5 | from email.mime.text import MIMEText  | 
            ||
| 6 | from email.utils import formataddr  | 
            ||
| 7 | from smtplib import SMTPRecipientsRefused  | 
            ||
| 8 | |||
| 9 | from lxml.html.diff import htmldiff  | 
            ||
| 10 | from mako.template import Template  | 
            ||
| 11 | from sqlalchemy.orm import Session  | 
            ||
| 12 | |||
| 13 | from tracim_backend.app_models.contents import CONTENT_TYPES  | 
            ||
| 14 | from tracim_backend.config import CFG  | 
            ||
| 15 | from tracim_backend.exceptions import EmptyNotificationError  | 
            ||
| 16 | from tracim_backend.lib.core.notifications import INotifier  | 
            ||
| 17 | from tracim_backend.lib.core.workspace import WorkspaceApi  | 
            ||
| 18 | from tracim_backend.lib.mail_notifier.sender import EmailSender  | 
            ||
| 19 | from tracim_backend.lib.mail_notifier.sender import send_email_through  | 
            ||
| 20 | from tracim_backend.lib.mail_notifier.utils import EST  | 
            ||
| 21 | from tracim_backend.lib.mail_notifier.utils import SmtpConfiguration  | 
            ||
| 22 | from tracim_backend.lib.utils.logger import logger  | 
            ||
| 23 | from tracim_backend.lib.utils.translation import Translator  | 
            ||
| 24 | from tracim_backend.lib.utils.utils import get_email_logo_frontend_url  | 
            ||
| 25 | from tracim_backend.lib.utils.utils import get_login_frontend_url  | 
            ||
| 26 | from tracim_backend.lib.utils.utils import get_reset_password_frontend_url  | 
            ||
| 27 | from tracim_backend.models.auth import User  | 
            ||
| 28 | from tracim_backend.models.context_models import ContentInContext  | 
            ||
| 29 | from tracim_backend.models.context_models import WorkspaceInContext  | 
            ||
| 30 | from tracim_backend.models.data import ActionDescription  | 
            ||
| 31 | from tracim_backend.models.data import Content  | 
            ||
| 32 | from tracim_backend.models.data import UserRoleInWorkspace  | 
            ||
| 33 | |||
| 34 | |||
| 35 | class EmailNotifier(INotifier):  | 
            ||
| 36 | """  | 
            ||
| 37 | EmailNotifier, this class will decide how to notify by mail  | 
            ||
| 38 | in order to let a EmailManager create email  | 
            ||
| 39 | """  | 
            ||
| 40 | |||
| 41 | def __init__(  | 
            ||
| 42 | self,  | 
            ||
| 43 | config: CFG,  | 
            ||
| 44 | session: Session,  | 
            ||
| 45 | current_user: User=None  | 
            ||
| 46 | ):  | 
            ||
| 47 | """  | 
            ||
| 48 | :param current_user: the user that has triggered the notification  | 
            ||
| 49 | :return:  | 
            ||
| 50 | """  | 
            ||
| 51 | INotifier.__init__(self, config, session, current_user)  | 
            ||
| 52 | logger.info(self, 'Instantiating Email Notifier')  | 
            ||
| 53 | |||
| 54 | self._user = current_user  | 
            ||
| 55 | self.session = session  | 
            ||
| 56 | self.config = config  | 
            ||
| 57 | self._smtp_config = SmtpConfiguration(  | 
            ||
| 58 | self.config.EMAIL_NOTIFICATION_SMTP_SERVER,  | 
            ||
| 59 | self.config.EMAIL_NOTIFICATION_SMTP_PORT,  | 
            ||
| 60 | self.config.EMAIL_NOTIFICATION_SMTP_USER,  | 
            ||
| 61 | self.config.EMAIL_NOTIFICATION_SMTP_PASSWORD  | 
            ||
| 62 | )  | 
            ||
| 63 | |||
| 64 | def notify_content_update(self, content: Content):  | 
            ||
| 65 | |||
| 66 | if content.get_last_action().id not \  | 
            ||
| 67 | in self.config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS:  | 
            ||
| 68 | logger.info(  | 
            ||
| 69 | self,  | 
            ||
| 70 |                 'Skip email notification for update of content {}' | 
            ||
| 71 |                 'by user {} (the action is {})'.format( | 
            ||
| 72 | content.content_id,  | 
            ||
| 73 | # below: 0 means "no user"  | 
            ||
| 74 | self._user.user_id if self._user else 0,  | 
            ||
| 75 | content.get_last_action().id  | 
            ||
| 76 | )  | 
            ||
| 77 | )  | 
            ||
| 78 | return  | 
            ||
| 79 | |||
| 80 | logger.info(self,  | 
            ||
| 81 | 'About to email-notify update'  | 
            ||
| 82 |                     'of content {} by user {}'.format( | 
            ||
| 83 | content.content_id,  | 
            ||
| 84 | # Below: 0 means "no user"  | 
            ||
| 85 | self._user.user_id if self._user else 0  | 
            ||
| 86 | )  | 
            ||
| 87 | )  | 
            ||
| 88 | |||
| 89 | if content.type not \  | 
            ||
| 90 | in self.config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS:  | 
            ||
| 91 | logger.info(  | 
            ||
| 92 | self,  | 
            ||
| 93 |                 'Skip email notification for update of content {}' | 
            ||
| 94 |                 'by user {} (the content type is {})'.format( | 
            ||
| 95 | content.type,  | 
            ||
| 96 | # below: 0 means "no user"  | 
            ||
| 97 | self._user.user_id if self._user else 0,  | 
            ||
| 98 | content.get_last_action().id  | 
            ||
| 99 | )  | 
            ||
| 100 | )  | 
            ||
| 101 | return  | 
            ||
| 102 | |||
| 103 | logger.info(self,  | 
            ||
| 104 | 'About to email-notify update'  | 
            ||
| 105 |                     'of content {} by user {}'.format( | 
            ||
| 106 | content.content_id,  | 
            ||
| 107 | # Below: 0 means "no user"  | 
            ||
| 108 | self._user.user_id if self._user else 0  | 
            ||
| 109 | )  | 
            ||
| 110 | )  | 
            ||
| 111 | |||
| 112 | ####  | 
            ||
| 113 | #  | 
            ||
| 114 | # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs.  | 
            ||
| 115 | # For that reason, we do not give SQLAlchemy objects but ids only  | 
            ||
| 116 | # (SQLA objects are related to a given thread/session)  | 
            ||
| 117 | #  | 
            ||
| 118 | try:  | 
            ||
| 119 | if self.config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower() == self.config.CST.ASYNC.lower():  | 
            ||
| 120 | logger.info(self, 'Sending email in ASYNC mode')  | 
            ||
| 121 | # TODO - D.A - 2014-11-06  | 
            ||
| 122 | # This feature must be implemented in order to be able to scale to large communities  | 
            ||
| 123 |                 raise NotImplementedError('Sending emails through ASYNC mode is not working yet') | 
            ||
| 124 | else:  | 
            ||
| 125 | logger.info(self, 'Sending email in SYNC mode')  | 
            ||
| 126 | EmailManager(  | 
            ||
| 127 | self._smtp_config,  | 
            ||
| 128 | self.config,  | 
            ||
| 129 | self.session,  | 
            ||
| 130 | ).notify_content_update(self._user.user_id, content.content_id)  | 
            ||
| 131 | except Exception as e:  | 
            ||
| 132 | # TODO - G.M - 2018-08-27 - Do Better catching for exception here  | 
            ||
| 133 |             logger.error(self, 'Exception catched during email notification: {}'.format(e.__str__())) | 
            ||
| 134 | logger.exception(self, e)  | 
            ||
| 135 | |||
| 136 | |||
| 137 | class EmailManager(object):  | 
            ||
| 138 | """  | 
            ||
| 139 | Compared to Notifier, this class is independant from the HTTP request thread  | 
            ||
| 140 | This class will build Email and send it for both created account and content  | 
            ||
| 141 | update  | 
            ||
| 142 | """  | 
            ||
| 143 | |||
| 144 | def __init__(  | 
            ||
| 145 | self,  | 
            ||
| 146 | smtp_config: SmtpConfiguration,  | 
            ||
| 147 | config: CFG,  | 
            ||
| 148 | session: Session  | 
            ||
| 149 | ) -> None:  | 
            ||
| 150 | self._smtp_config = smtp_config  | 
            ||
| 151 | self.config = config  | 
            ||
| 152 | self.session = session  | 
            ||
| 153 | # FIXME - G.M - We need to have a session for the emailNotifier  | 
            ||
| 154 | |||
| 155 | # if not self.session:  | 
            ||
| 156 | # engine = get_engine(settings)  | 
            ||
| 157 | # session_factory = get_session_factory(engine)  | 
            ||
| 158 | # app_config = CFG(settings)  | 
            ||
| 159 | |||
| 160 | def _get_sender(self, user: User=None) -> str:  | 
            ||
| 161 | """  | 
            ||
| 162 | Return sender string like "Bob Dylan  | 
            ||
| 163 | (via Tracim) <[email protected]>"  | 
            ||
| 164 | :param user: user to extract display name  | 
            ||
| 165 | :return: sender string  | 
            ||
| 166 | """  | 
            ||
| 167 | |||
| 168 | email_template = self.config.EMAIL_NOTIFICATION_FROM_EMAIL  | 
            ||
| 169 | mail_sender_name = self.config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL # nopep8  | 
            ||
| 170 | if user:  | 
            ||
| 171 |             mail_sender_name = '{name} via Tracim'.format(name=user.display_name) | 
            ||
| 172 |             email_address = email_template.replace('{user_id}', str(user.user_id)) | 
            ||
| 173 | # INFO - D.A. - 2017-08-04  | 
            ||
| 174 | # We use email_template.replace() instead of .format() because this  | 
            ||
| 175 | # method is more robust to errors in config file.  | 
            ||
| 176 | #  | 
            ||
| 177 |             # For example, if the email is info+{userid}@tracim.fr | 
            ||
| 178 | # email.format(user_id='bob') will raise an exception  | 
            ||
| 179 |             # email.replace('{user_id}', 'bob') will just ignore {userid} | 
            ||
| 180 | else:  | 
            ||
| 181 |             email_address = email_template.replace('{user_id}', '0') | 
            ||
| 182 | |||
| 183 | return formataddr((mail_sender_name, email_address))  | 
            ||
| 184 | |||
| 185 | # Content Notification  | 
            ||
| 186 | |||
| 187 | @staticmethod  | 
            ||
| 188 | def log_notification(  | 
            ||
| 189 | config: CFG,  | 
            ||
| 190 | action: str,  | 
            ||
| 191 | recipient: typing.Optional[str],  | 
            ||
| 192 | subject: typing.Optional[str],  | 
            ||
| 193 | ) -> None:  | 
            ||
| 194 | """Log notification metadata."""  | 
            ||
| 195 | log_path = config.EMAIL_NOTIFICATION_LOG_FILE_PATH  | 
            ||
| 196 | if log_path:  | 
            ||
| 197 | # TODO - A.P - 2017-09-06 - file logging inefficiency  | 
            ||
| 198 | # Updating a document with 100 users to notify will leads to open  | 
            ||
| 199 | # and close the file 100 times.  | 
            ||
| 200 | with open(log_path, 'a') as log_file:  | 
            ||
| 201 | print(  | 
            ||
| 202 | datetime.datetime.now(),  | 
            ||
| 203 | action,  | 
            ||
| 204 | recipient,  | 
            ||
| 205 | subject,  | 
            ||
| 206 | sep='|',  | 
            ||
| 207 | file=log_file,  | 
            ||
| 208 | )  | 
            ||
| 209 | |||
| 210 | def notify_content_update(  | 
            ||
| 211 | self,  | 
            ||
| 212 | event_actor_id: int,  | 
            ||
| 213 | event_content_id: int  | 
            ||
| 214 | ) -> None:  | 
            ||
| 215 | """  | 
            ||
| 216 | Look for all users to be notified about the new content and send them an  | 
            ||
| 217 | individual email  | 
            ||
| 218 | :param event_actor_id: id of the user that has triggered the event  | 
            ||
| 219 | :param event_content_id: related content_id  | 
            ||
| 220 | :return:  | 
            ||
| 221 | """  | 
            ||
| 222 | # FIXME - D.A. - 2014-11-05  | 
            ||
| 223 | # Dirty import. It's here in order to avoid circular import  | 
            ||
| 224 | from tracim_backend.lib.core.content import ContentApi  | 
            ||
| 225 | from tracim_backend.lib.core.user import UserApi  | 
            ||
| 226 | user = UserApi(  | 
            ||
| 227 | None,  | 
            ||
| 228 | config=self.config,  | 
            ||
| 229 | session=self.session,  | 
            ||
| 230 | ).get_one(event_actor_id)  | 
            ||
| 231 |         logger.debug(self, 'Content: {}'.format(event_content_id)) | 
            ||
| 232 | content_api = ContentApi(  | 
            ||
| 233 | current_user=user,  | 
            ||
| 234 | session=self.session,  | 
            ||
| 235 | config=self.config,  | 
            ||
| 236 | )  | 
            ||
| 237 | content = ContentApi(  | 
            ||
| 238 | session=self.session,  | 
            ||
| 239 | current_user=user, # nopep8 TODO - use a system user instead of the user that has triggered the event  | 
            ||
| 240 | config=self.config,  | 
            ||
| 241 | show_archived=True,  | 
            ||
| 242 | show_deleted=True,  | 
            ||
| 243 | ).get_one(event_content_id, CONTENT_TYPES.Any_SLUG)  | 
            ||
| 244 | workspace_api = WorkspaceApi(  | 
            ||
| 245 | session=self.session,  | 
            ||
| 246 | current_user=user,  | 
            ||
| 247 | config=self.config,  | 
            ||
| 248 | )  | 
            ||
| 249 | workpace_in_context = workspace_api.get_workspace_with_context(workspace_api.get_one(content.workspace_id)) # nopep8  | 
            ||
| 250 | main_content = content.parent if content.type == CONTENT_TYPES.Comment.slug else content # nopep8  | 
            ||
| 251 | notifiable_roles = WorkspaceApi(  | 
            ||
| 252 | current_user=user,  | 
            ||
| 253 | session=self.session,  | 
            ||
| 254 | config=self.config,  | 
            ||
| 255 | ).get_notifiable_roles(content.workspace)  | 
            ||
| 256 | |||
| 257 | if len(notifiable_roles) <= 0:  | 
            ||
| 258 |             logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label)) | 
            ||
| 259 | return  | 
            ||
| 260 | |||
| 261 | |||
| 262 |         logger.info(self, 'Sending asynchronous emails to {} user(s)'.format(len(notifiable_roles))) | 
            ||
| 263 | # INFO - D.A. - 2014-11-06  | 
            ||
| 264 | # The following email sender will send emails in the async task queue  | 
            ||
| 265 | # This allow to build all mails through current thread but really send them (including SMTP connection)  | 
            ||
| 266 | # In the other thread.  | 
            ||
| 267 | #  | 
            ||
| 268 | # This way, the webserver will return sooner (actually before notification emails are sent  | 
            ||
| 269 | async_email_sender = EmailSender(  | 
            ||
| 270 | self.config,  | 
            ||
| 271 | self._smtp_config,  | 
            ||
| 272 | self.config.EMAIL_NOTIFICATION_ACTIVATED  | 
            ||
| 273 | )  | 
            ||
| 274 | for role in notifiable_roles:  | 
            ||
| 275 |             logger.info(self, 'Sending email to {}'.format(role.user.email)) | 
            ||
| 276 | translator = Translator(app_config=self.config, default_lang=role.user.lang) # nopep8  | 
            ||
| 277 | _ = translator.get_translation  | 
            ||
| 278 | to_addr = formataddr((role.user.display_name, role.user.email))  | 
            ||
| 279 | # INFO - G.M - 2017-11-15 - set content_id in header to permit reply  | 
            ||
| 280 | # references can have multiple values, but only one in this case.  | 
            ||
| 281 | replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8  | 
            ||
| 282 |                 '{content_id}', str(content.content_id) | 
            ||
| 283 | )  | 
            ||
| 284 | |||
| 285 | reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8  | 
            ||
| 286 |                 '{content_id}',str(content.content_id) | 
            ||
| 287 | )  | 
            ||
| 288 | #  | 
            ||
| 289 | # INFO - D.A. - 2014-11-06  | 
            ||
| 290 | # We do not use .format() here because the subject defined in the .ini file  | 
            ||
| 291 | # may not include all required labels. In order to avoid partial format() (which result in an exception)  | 
            ||
| 292 | # we do use replace and force the use of .__str__() in order to process LazyString objects  | 
            ||
| 293 | #  | 
            ||
| 294 | subject = self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT  | 
            ||
| 295 | subject = subject.replace(EST.WEBSITE_TITLE, self.config.WEBSITE_TITLE.__str__())  | 
            ||
| 296 | subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__())  | 
            ||
| 297 | subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__())  | 
            ||
| 298 | subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__())  | 
            ||
| 299 |             reply_to_label = _('{username} & all members of {workspace}').format(  # nopep8 | 
            ||
| 300 | username=user.display_name,  | 
            ||
| 301 | workspace=main_content.workspace.label)  | 
            ||
| 302 | |||
| 303 |             message = MIMEMultipart('alternative') | 
            ||
| 304 | message['Subject'] = subject  | 
            ||
| 305 | message['From'] = self._get_sender(user)  | 
            ||
| 306 | message['To'] = to_addr  | 
            ||
| 307 | message['Reply-to'] = formataddr((reply_to_label, replyto_addr))  | 
            ||
| 308 | # INFO - G.M - 2017-11-15  | 
            ||
| 309 | # References can theorically have label, but in pratice, references  | 
            ||
| 310 | # contains only message_id from parents post in thread.  | 
            ||
| 311 | # To link this email to a content we create a virtual parent  | 
            ||
| 312 | # in reference who contain the content_id.  | 
            ||
| 313 |             message['References'] = formataddr(('', reference_addr)) | 
            ||
| 314 | content_in_context = content_api.get_content_in_context(content)  | 
            ||
| 315 | parent_in_context = None  | 
            ||
| 316 | if content.parent_id:  | 
            ||
| 317 | parent_in_context = content_api.get_content_in_context(content.parent) # nopep8  | 
            ||
| 318 | body_text = self._build_email_body_for_content(  | 
            ||
| 319 | self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT,  | 
            ||
| 320 | role,  | 
            ||
| 321 | content_in_context,  | 
            ||
| 322 | parent_in_context,  | 
            ||
| 323 | workpace_in_context,  | 
            ||
| 324 | user,  | 
            ||
| 325 | translator,  | 
            ||
| 326 | )  | 
            ||
| 327 | body_html = self._build_email_body_for_content(  | 
            ||
| 328 | self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML,  | 
            ||
| 329 | role,  | 
            ||
| 330 | content_in_context,  | 
            ||
| 331 | parent_in_context,  | 
            ||
| 332 | workpace_in_context,  | 
            ||
| 333 | user,  | 
            ||
| 334 | translator,  | 
            ||
| 335 | )  | 
            ||
| 336 | |||
| 337 | part1 = MIMEText(body_text, 'plain', 'utf-8')  | 
            ||
| 338 | part2 = MIMEText(body_html, 'html', 'utf-8')  | 
            ||
| 339 | # Attach parts into message container.  | 
            ||
| 340 | # According to RFC 2046, the last part of a multipart message, in this case  | 
            ||
| 341 | # the HTML message, is best and preferred.  | 
            ||
| 342 | message.attach(part1)  | 
            ||
| 343 | message.attach(part2)  | 
            ||
| 344 | |||
| 345 | self.log_notification(  | 
            ||
| 346 | action='CREATED',  | 
            ||
| 347 | recipient=message['To'],  | 
            ||
| 348 | subject=message['Subject'],  | 
            ||
| 349 | config=self.config,  | 
            ||
| 350 | )  | 
            ||
| 351 | |||
| 352 | send_email_through(  | 
            ||
| 353 | self.config,  | 
            ||
| 354 | async_email_sender.send_mail,  | 
            ||
| 355 | message  | 
            ||
| 356 | )  | 
            ||
| 357 | |||
| 358 | View Code Duplication | def notify_created_account(  | 
            |
| 
                                                                                                    
                        
                         | 
                |||
| 359 | self,  | 
            ||
| 360 | user: User,  | 
            ||
| 361 | password: str,  | 
            ||
| 362 | ) -> None:  | 
            ||
| 363 | """  | 
            ||
| 364 | Send created account email to given user.  | 
            ||
| 365 | |||
| 366 | :param password: choosed password  | 
            ||
| 367 | :param user: user to notify  | 
            ||
| 368 | """  | 
            ||
| 369 | # TODO BS 20160712: Cyclic import  | 
            ||
| 370 |         logger.debug(self, 'user: {}'.format(user.user_id)) | 
            ||
| 371 |         logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format( | 
            ||
| 372 | user.email,  | 
            ||
| 373 | ))  | 
            ||
| 374 | |||
| 375 | async_email_sender = EmailSender(  | 
            ||
| 376 | self.config,  | 
            ||
| 377 | self._smtp_config,  | 
            ||
| 378 | self.config.EMAIL_NOTIFICATION_ACTIVATED  | 
            ||
| 379 | )  | 
            ||
| 380 | |||
| 381 | subject = \  | 
            ||
| 382 | self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \  | 
            ||
| 383 | .replace(  | 
            ||
| 384 | EST.WEBSITE_TITLE,  | 
            ||
| 385 | str(self.config.WEBSITE_TITLE)  | 
            ||
| 386 | )  | 
            ||
| 387 |         message = MIMEMultipart('alternative') | 
            ||
| 388 | message['Subject'] = subject  | 
            ||
| 389 | message['From'] = self._get_sender()  | 
            ||
| 390 | message['To'] = formataddr((user.get_display_name(), user.email))  | 
            ||
| 391 | |||
| 392 | text_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT # nopep8  | 
            ||
| 393 | html_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML # nopep8  | 
            ||
| 394 | |||
| 395 |         context = { | 
            ||
| 396 | 'user': user,  | 
            ||
| 397 | 'password': password,  | 
            ||
| 398 | 'logo_url': get_email_logo_frontend_url(self.config),  | 
            ||
| 399 | 'login_url': get_login_frontend_url(self.config),  | 
            ||
| 400 | }  | 
            ||
| 401 | translator = Translator(self.config, default_lang=user.lang)  | 
            ||
| 402 | body_text = self._render_template(  | 
            ||
| 403 | mako_template_filepath=text_template_file_path,  | 
            ||
| 404 | context=context,  | 
            ||
| 405 | translator=translator  | 
            ||
| 406 | )  | 
            ||
| 407 | |||
| 408 | body_html = self._render_template(  | 
            ||
| 409 | mako_template_filepath=html_template_file_path,  | 
            ||
| 410 | context=context,  | 
            ||
| 411 | translator=translator  | 
            ||
| 412 | )  | 
            ||
| 413 | |||
| 414 | part1 = MIMEText(body_text, 'plain', 'utf-8')  | 
            ||
| 415 | part2 = MIMEText(body_html, 'html', 'utf-8')  | 
            ||
| 416 | |||
| 417 | # Attach parts into message container.  | 
            ||
| 418 | # According to RFC 2046, the last part of a multipart message,  | 
            ||
| 419 | # in this case the HTML message, is best and preferred.  | 
            ||
| 420 | message.attach(part1)  | 
            ||
| 421 | message.attach(part2)  | 
            ||
| 422 | |||
| 423 | send_email_through(  | 
            ||
| 424 | config=self.config,  | 
            ||
| 425 | sendmail_callable=async_email_sender.send_mail,  | 
            ||
| 426 | message=message  | 
            ||
| 427 | )  | 
            ||
| 428 | |||
| 429 | View Code Duplication | def notify_reset_password(  | 
            |
| 430 | self,  | 
            ||
| 431 | user: User,  | 
            ||
| 432 | reset_password_token: str,  | 
            ||
| 433 | ) -> None:  | 
            ||
| 434 | """  | 
            ||
| 435 | Reset password link for user  | 
            ||
| 436 | :param user: user to notify  | 
            ||
| 437 | :param reset_password_token: token for resetting password  | 
            ||
| 438 | """  | 
            ||
| 439 |         logger.debug(self, 'user: {}'.format(user.user_id)) | 
            ||
| 440 |         logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format( | 
            ||
| 441 | user.email,  | 
            ||
| 442 | ))  | 
            ||
| 443 | translator = Translator(self.config, default_lang=user.lang)  | 
            ||
| 444 | async_email_sender = EmailSender(  | 
            ||
| 445 | self.config,  | 
            ||
| 446 | self._smtp_config,  | 
            ||
| 447 | self.config.EMAIL_NOTIFICATION_ACTIVATED  | 
            ||
| 448 | )  | 
            ||
| 449 | subject = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_SUBJECT.replace(  | 
            ||
| 450 | EST.WEBSITE_TITLE,  | 
            ||
| 451 | str(self.config.WEBSITE_TITLE)  | 
            ||
| 452 | )  | 
            ||
| 453 |         message = MIMEMultipart('alternative') | 
            ||
| 454 | message['Subject'] = subject  | 
            ||
| 455 | message['From'] = self._get_sender()  | 
            ||
| 456 | message['To'] = formataddr((user.get_display_name(), user.email))  | 
            ||
| 457 | |||
| 458 | text_template_file_path = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_TEXT # nopep8  | 
            ||
| 459 | html_template_file_path = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_HTML # nopep8  | 
            ||
| 460 | # TODO - G.M - 2018-08-17 - Generate token  | 
            ||
| 461 |         context = { | 
            ||
| 462 | 'user': user,  | 
            ||
| 463 | 'logo_url': get_email_logo_frontend_url(self.config),  | 
            ||
| 464 | 'reset_password_url': get_reset_password_frontend_url(  | 
            ||
| 465 | self.config,  | 
            ||
| 466 | token=reset_password_token,  | 
            ||
| 467 | email=user.email,  | 
            ||
| 468 | ),  | 
            ||
| 469 | }  | 
            ||
| 470 | body_text = self._render_template(  | 
            ||
| 471 | mako_template_filepath=text_template_file_path,  | 
            ||
| 472 | context=context,  | 
            ||
| 473 | translator=translator,  | 
            ||
| 474 | )  | 
            ||
| 475 | |||
| 476 | body_html = self._render_template(  | 
            ||
| 477 | mako_template_filepath=html_template_file_path,  | 
            ||
| 478 | context=context,  | 
            ||
| 479 | translator=translator,  | 
            ||
| 480 | )  | 
            ||
| 481 | |||
| 482 | part1 = MIMEText(body_text, 'plain', 'utf-8')  | 
            ||
| 483 | part2 = MIMEText(body_html, 'html', 'utf-8')  | 
            ||
| 484 | |||
| 485 | # Attach parts into message container.  | 
            ||
| 486 | # According to RFC 2046, the last part of a multipart message,  | 
            ||
| 487 | # in this case the HTML message, is best and preferred.  | 
            ||
| 488 | message.attach(part1)  | 
            ||
| 489 | message.attach(part2)  | 
            ||
| 490 | |||
| 491 | send_email_through(  | 
            ||
| 492 | config=self.config,  | 
            ||
| 493 | sendmail_callable=async_email_sender.send_mail,  | 
            ||
| 494 | message=message  | 
            ||
| 495 | )  | 
            ||
| 496 | |||
| 497 | def _render_template(  | 
            ||
| 498 | self,  | 
            ||
| 499 | mako_template_filepath: str,  | 
            ||
| 500 | context: dict,  | 
            ||
| 501 | translator: Translator  | 
            ||
| 502 | ) -> str:  | 
            ||
| 503 | """  | 
            ||
| 504 | Render mako template with all needed current variables.  | 
            ||
| 505 | |||
| 506 | :param mako_template_filepath: file path of mako template  | 
            ||
| 507 | :param context: dict with template context  | 
            ||
| 508 | :return: template rendered string  | 
            ||
| 509 | """  | 
            ||
| 510 | |||
| 511 | template = Template(filename=mako_template_filepath)  | 
            ||
| 512 | return template.render(  | 
            ||
| 513 | _=translator.get_translation,  | 
            ||
| 514 | config=self.config,  | 
            ||
| 515 | **context  | 
            ||
| 516 | )  | 
            ||
| 517 | |||
| 518 | def _build_context_for_content_update(  | 
            ||
| 519 | self,  | 
            ||
| 520 | role: UserRoleInWorkspace,  | 
            ||
| 521 | content_in_context: ContentInContext,  | 
            ||
| 522 | parent_in_context: typing.Optional[ContentInContext],  | 
            ||
| 523 | workspace_in_context: WorkspaceInContext,  | 
            ||
| 524 | actor: User,  | 
            ||
| 525 | translator: Translator  | 
            ||
| 526 | ):  | 
            ||
| 527 | |||
| 528 | _ = translator.get_translation  | 
            ||
| 529 | content = content_in_context.content  | 
            ||
| 530 | action = content.get_last_action().id  | 
            ||
| 531 | |||
| 532 | # default values  | 
            ||
| 533 | user = role.user  | 
            ||
| 534 | workspace = role.workspace  | 
            ||
| 535 | workspace_url = workspace_in_context.frontend_url  | 
            ||
| 536 | main_title = content.label  | 
            ||
| 537 | status_label = content.get_status().label  | 
            ||
| 538 | # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url # nopep8  | 
            ||
| 539 | status_icon_url = ''  | 
            ||
| 540 | role_label = role.role_as_label()  | 
            ||
| 541 |         content_intro = '<span id="content-intro-username">{}</span> did something.'.format(actor.display_name)  # nopep8 | 
            ||
| 542 | content_text = content.description  | 
            ||
| 543 | call_to_action_text = 'See more'  | 
            ||
| 544 | call_to_action_url = content_in_context.frontend_url  | 
            ||
| 545 | logo_url = get_email_logo_frontend_url(self.config)  | 
            ||
| 546 | |||
| 547 | if ActionDescription.CREATION == action:  | 
            ||
| 548 |             call_to_action_text = _('View online') | 
            ||
| 549 |             content_intro = _('<span id="content-intro-username">{}</span> create a content:').format(actor.display_name)  # nopep8 | 
            ||
| 550 | |||
| 551 | if CONTENT_TYPES.Thread.slug == content.type:  | 
            ||
| 552 | if content.get_last_comment_from(actor):  | 
            ||
| 553 | content_text = content.get_last_comment_from(actor).description # nopep8  | 
            ||
| 554 | |||
| 555 |                 call_to_action_text = _('Answer') | 
            ||
| 556 |                 content_intro = _('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name) | 
            ||
| 557 |                 content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + content_text  # nopep8 | 
            ||
| 558 | |||
| 559 | elif CONTENT_TYPES.File.slug == content.type:  | 
            ||
| 560 |                 content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name) | 
            ||
| 561 | if content.description:  | 
            ||
| 562 | content_text = content.description  | 
            ||
| 563 | else:  | 
            ||
| 564 |                     content_text = '<span id="content-body-only-title">{}</span>'.format(content.label) | 
            ||
| 565 | |||
| 566 | elif CONTENT_TYPES.Page.slug == content.type:  | 
            ||
| 567 |                 content_intro = _('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name) | 
            ||
| 568 |                 content_text = '<span id="content-body-only-title">{}</span>'.format(content.label) | 
            ||
| 569 | |||
| 570 | elif ActionDescription.REVISION == action:  | 
            ||
| 571 | content_text = content.description  | 
            ||
| 572 |             call_to_action_text = _('View online') | 
            ||
| 573 | |||
| 574 | if CONTENT_TYPES.File.slug == content.type:  | 
            ||
| 575 |                 content_intro = _('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name) | 
            ||
| 576 | content_text = content.description  | 
            ||
| 577 | |||
| 578 | elif ActionDescription.EDITION == action:  | 
            ||
| 579 |             call_to_action_text = _('View online') | 
            ||
| 580 | |||
| 581 | if CONTENT_TYPES.File.slug == content.type:  | 
            ||
| 582 |                 content_intro = _('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name) | 
            ||
| 583 |                 content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + content.description  # nopep8 | 
            ||
| 584 | |||
| 585 | elif CONTENT_TYPES.Thread.slug == content.type:  | 
            ||
| 586 |                 content_intro = _('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name) | 
            ||
| 587 | previous_revision = content.get_previous_revision()  | 
            ||
| 588 | title_diff = ''  | 
            ||
| 589 | if previous_revision.label != content.label:  | 
            ||
| 590 | title_diff = htmldiff(previous_revision.label, content.label)  | 
            ||
| 591 |                 content_text = str('<p id="content-body-intro">{}</p> {text} {title_diff} {content_diff}').format( | 
            ||
| 592 |                     text=_('Here is an overview of the changes:'), | 
            ||
| 593 | title_diff=title_diff,  | 
            ||
| 594 | content_diff=htmldiff(previous_revision.description, content.description)  | 
            ||
| 595 | )  | 
            ||
| 596 | elif CONTENT_TYPES.Page.slug == content.type:  | 
            ||
| 597 |                 content_intro = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name) | 
            ||
| 598 | previous_revision = content.get_previous_revision()  | 
            ||
| 599 | title_diff = ''  | 
            ||
| 600 | if previous_revision.label != content.label:  | 
            ||
| 601 | title_diff = htmldiff(previous_revision.label, content.label) # nopep8  | 
            ||
| 602 |                 content_text = str('<p id="content-body-intro">{}</p> {text}</p> {title_diff} {content_diff}').format(  # nopep8 | 
            ||
| 603 | actor.display_name,  | 
            ||
| 604 |                     text=_('Here is an overview of the changes:'), | 
            ||
| 605 | title_diff=title_diff,  | 
            ||
| 606 | content_diff=htmldiff(previous_revision.description, content.description)  | 
            ||
| 607 | )  | 
            ||
| 608 | |||
| 609 | elif ActionDescription.STATUS_UPDATE == action:  | 
            ||
| 610 | intro_user_msg = _(  | 
            ||
| 611 |                 '<span id="content-intro-username">{}</span> ' | 
            ||
| 612 | 'updated the following status:'  | 
            ||
| 613 | )  | 
            ||
| 614 |             intro_body_msg = '<p id="content-body-intro">{}: {}</p>' | 
            ||
| 615 | |||
| 616 |             call_to_action_text = _('View online') | 
            ||
| 617 | content_intro = intro_user_msg.format(actor.display_name)  | 
            ||
| 618 | content_text = intro_body_msg.format(  | 
            ||
| 619 | content.get_label(),  | 
            ||
| 620 | content.get_status().label,  | 
            ||
| 621 | )  | 
            ||
| 622 | |||
| 623 | elif ActionDescription.COMMENT == action:  | 
            ||
| 624 |             call_to_action_text = _('Answer') | 
            ||
| 625 | main_title = parent_in_context.label  | 
            ||
| 626 |             content_intro = _('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)  # nopep8 | 
            ||
| 627 | call_to_action_url = parent_in_context.frontend_url  | 
            ||
| 628 | |||
| 629 | if not content_intro and not content_text:  | 
            ||
| 630 | # Skip notification, but it's not normal  | 
            ||
| 631 | logger.error(  | 
            ||
| 632 | self,  | 
            ||
| 633 | 'A notification is being sent but no content. '  | 
            ||
| 634 |                 'Here are some debug informations: [content_id: {cid}]' | 
            ||
| 635 |                 '[action: {act}][author: {actor}]'.format( | 
            ||
| 636 | cid=content.content_id,  | 
            ||
| 637 | act=action,  | 
            ||
| 638 | actor=actor  | 
            ||
| 639 | )  | 
            ||
| 640 | )  | 
            ||
| 641 |             raise EmptyNotificationError('Unexpected empty notification') | 
            ||
| 642 | |||
| 643 | # FIXME: remove/readapt assert to debug easily broken case  | 
            ||
| 644 | assert user  | 
            ||
| 645 | assert workspace  | 
            ||
| 646 | assert main_title  | 
            ||
| 647 | assert status_label  | 
            ||
| 648 | # assert status_icon_url  | 
            ||
| 649 | assert role_label  | 
            ||
| 650 | assert content_intro  | 
            ||
| 651 | assert content_text or content_text == content.description  | 
            ||
| 652 | assert call_to_action_text  | 
            ||
| 653 | assert call_to_action_url  | 
            ||
| 654 | assert logo_url  | 
            ||
| 655 | |||
| 656 |         return { | 
            ||
| 657 | 'user': role.user,  | 
            ||
| 658 | 'workspace': role.workspace,  | 
            ||
| 659 | 'workspace_url': workspace_url,  | 
            ||
| 660 | 'main_title': main_title,  | 
            ||
| 661 | 'status_label': status_label,  | 
            ||
| 662 | 'status_icon_url': status_icon_url,  | 
            ||
| 663 | 'role_label': role_label,  | 
            ||
| 664 | 'content_intro': content_intro,  | 
            ||
| 665 | 'content_text': content_text,  | 
            ||
| 666 | 'call_to_action_text': call_to_action_text,  | 
            ||
| 667 | 'call_to_action_url': call_to_action_url,  | 
            ||
| 668 | 'logo_url': logo_url,  | 
            ||
| 669 | }  | 
            ||
| 670 | |||
| 671 | def _build_email_body_for_content(  | 
            ||
| 672 | self,  | 
            ||
| 673 | mako_template_filepath: str,  | 
            ||
| 674 | role: UserRoleInWorkspace,  | 
            ||
| 675 | content_in_context: ContentInContext,  | 
            ||
| 676 | parent_in_context: typing.Optional[ContentInContext],  | 
            ||
| 677 | workspace_in_context: WorkspaceInContext,  | 
            ||
| 678 | actor: User,  | 
            ||
| 679 | translator: Translator  | 
            ||
| 680 | ) -> str:  | 
            ||
| 681 | """  | 
            ||
| 682 | Build an email body and return it as a string  | 
            ||
| 683 | :param mako_template_filepath: the absolute path to the mako template  | 
            ||
| 684 | to be used for email body building  | 
            ||
| 685 | :param role: the role related to user to whom the email must be sent.  | 
            ||
| 686 | The role is required (and not the user only) in order to show in the  | 
            ||
| 687 | mail why the user receive the notification  | 
            ||
| 688 | :param content_in_context: the content item related to the notification  | 
            ||
| 689 | :param parent_in_context: parent of the content item related to the  | 
            ||
| 690 | notification  | 
            ||
| 691 | :param actor: the user at the origin of the action / notification  | 
            ||
| 692 | (for example the one who wrote a comment  | 
            ||
| 693 | :return: the built email body as string. In case of multipart email,  | 
            ||
| 694 | this method must be called one time for text and one time for html  | 
            ||
| 695 | """  | 
            ||
| 696 |         logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))  # nopep8 | 
            ||
| 697 | context = self._build_context_for_content_update(  | 
            ||
| 698 | role=role,  | 
            ||
| 699 | content_in_context=content_in_context,  | 
            ||
| 700 | parent_in_context=parent_in_context,  | 
            ||
| 701 | workspace_in_context=workspace_in_context,  | 
            ||
| 702 | actor=actor,  | 
            ||
| 703 | translator=translator  | 
            ||
| 704 | )  | 
            ||
| 705 | body_content = self._render_template(  | 
            ||
| 706 | mako_template_filepath=mako_template_filepath,  | 
            ||
| 707 | context=context,  | 
            ||
| 708 | translator=translator,  | 
            ||
| 709 | )  | 
            ||
| 710 | return body_content  | 
            ||
| 711 | |||
| 712 | |||
| 713 | def get_email_manager(config: CFG, session: Session):  | 
            ||
| 714 | """  | 
            ||
| 715 | :return: EmailManager instance  | 
            ||
| 716 | """  | 
            ||
| 717 | # TODO: Find a way to import properly without cyclic import  | 
            ||
| 718 | |||
| 719 | smtp_config = SmtpConfiguration(  | 
            ||
| 720 | config.EMAIL_NOTIFICATION_SMTP_SERVER,  | 
            ||
| 721 | config.EMAIL_NOTIFICATION_SMTP_PORT,  | 
            ||
| 722 | config.EMAIL_NOTIFICATION_SMTP_USER,  | 
            ||
| 723 | config.EMAIL_NOTIFICATION_SMTP_PASSWORD  | 
            ||
| 724 | )  | 
            ||
| 725 | |||
| 726 | return EmailManager(config=config, smtp_config=smtp_config, session=session)  | 
            ||
| 727 |