EmailManager._build_context_for_content_update()   F
last analyzed

Complexity

Conditions 19

Size

Total Lines 151
Code Lines 118

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 118
dl 0
loc 151
rs 0.4199
c 0
b 0
f 0
cc 19
nop 7

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like notifier.EmailManager._build_context_for_content_update() 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_type_list
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_type_list.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_type_list.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(main_content.content_id)
283
            )
284
285
            reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8
286
                '{content_id}',str(main_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(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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_type_list.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_type_list.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_type_list.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_type_list.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_type_list.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_type_list.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_type_list.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