Passed
Pull Request — develop (#31)
by inkhey
01:53
created

notifier.EmailManager.notify_reset_password()   B

Complexity

Conditions 1

Size

Total Lines 62
Code Lines 41

Duplication

Lines 62
Ratio 100 %

Importance

Changes 0
Metric Value
eloc 41
dl 62
loc 62
rs 8.896
c 0
b 0
f 0
cc 1
nop 3

How to fix   Long Method   

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:

1
# -*- coding: utf-8 -*-
2
import datetime
3
import traceback
4
import typing
5
6
from email.mime.multipart import MIMEMultipart
7
from email.mime.text import MIMEText
8
from email.utils import formataddr
9
from smtplib import SMTPRecipientsRefused
10
11
from lxml.html.diff import htmldiff
12
from mako.template import Template
13
from sqlalchemy.orm import Session
14
15
from tracim_backend.config import CFG
16
from tracim_backend.lib.core.notifications import INotifier
17
from tracim_backend.lib.mail_notifier.sender import EmailSender
18
from tracim_backend.lib.mail_notifier.utils import SmtpConfiguration, EST
19
from tracim_backend.lib.mail_notifier.sender import send_email_through
20
from tracim_backend.lib.core.workspace import WorkspaceApi
21
from tracim_backend.lib.utils.logger import logger
22
from tracim_backend.lib.utils.utils import get_login_frontend_url
23
from tracim_backend.lib.utils.utils import get_reset_password_frontend_url
24
from tracim_backend.lib.utils.utils import get_email_logo_frontend_url
25
from tracim_backend.models.auth import User
26
from tracim_backend.app_models.contents import CONTENT_TYPES
27
from tracim_backend.models.context_models import ContentInContext
28
from tracim_backend.models.context_models import WorkspaceInContext
29
from tracim_backend.models.data import ActionDescription
30
from tracim_backend.models.data import Content
31
from tracim_backend.models.data import UserRoleInWorkspace
32
from tracim_backend.lib.utils.translation import Translator
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.error(self, traceback.format_exc())
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
            body_text = self._build_email_body_for_content(
316
                self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT,
317
                role,
318
                content_in_context,
319
                workpace_in_context,
320
                user,
321
                translator
322
            )
323
            body_html = self._build_email_body_for_content(
324
                self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML,
325
                role,
326
                content_in_context,
327
                workpace_in_context,
328
                user,
329
                translator
330
            )
331
332
            part1 = MIMEText(body_text, 'plain', 'utf-8')
333
            part2 = MIMEText(body_html, 'html', 'utf-8')
334
            # Attach parts into message container.
335
            # According to RFC 2046, the last part of a multipart message, in this case
336
            # the HTML message, is best and preferred.
337
            message.attach(part1)
338
            message.attach(part2)
339
340
            self.log_notification(
341
                action='CREATED',
342
                recipient=message['To'],
343
                subject=message['Subject'],
344
                config=self.config,
345
            )
346
347
            send_email_through(
348
                self.config,
349
                async_email_sender.send_mail,
350
                message
351
            )
352
353 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...
354
            self,
355
            user: User,
356
            password: str,
357
    ) -> None:
358
        """
359
        Send created account email to given user.
360
361
        :param password: choosed password
362
        :param user: user to notify
363
        """
364
        # TODO BS 20160712: Cyclic import
365
        logger.debug(self, 'user: {}'.format(user.user_id))
366
        logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format(
367
            user.email,
368
        ))
369
370
        async_email_sender = EmailSender(
371
            self.config,
372
            self._smtp_config,
373
            self.config.EMAIL_NOTIFICATION_ACTIVATED
374
        )
375
376
        subject = \
377
            self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \
378
            .replace(
379
                EST.WEBSITE_TITLE,
380
                str(self.config.WEBSITE_TITLE)
381
            )
382
        message = MIMEMultipart('alternative')
383
        message['Subject'] = subject
384
        message['From'] = self._get_sender()
385
        message['To'] = formataddr((user.get_display_name(), user.email))
386
387
        text_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT  # nopep8
388
        html_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML  # nopep8
389
390
        context = {
391
            'user': user,
392
            'password': password,
393
            'logo_url': get_email_logo_frontend_url(self.config),
394
            'login_url': get_login_frontend_url(self.config),
395
        }
396
        translator = Translator(self.config, default_lang=user.lang)
397
        body_text = self._render_template(
398
            mako_template_filepath=text_template_file_path,
399
            context=context,
400
            translator=translator
401
        )
402
403
        body_html = self._render_template(
404
            mako_template_filepath=html_template_file_path,
405
            context=context,
406
            translator=translator
407
        )
408
409
        part1 = MIMEText(body_text, 'plain', 'utf-8')
410
        part2 = MIMEText(body_html, 'html', 'utf-8')
411
412
        # Attach parts into message container.
413
        # According to RFC 2046, the last part of a multipart message,
414
        # in this case the HTML message, is best and preferred.
415
        message.attach(part1)
416
        message.attach(part2)
417
418
        send_email_through(
419
            config=self.config,
420
            sendmail_callable=async_email_sender.send_mail,
421
            message=message
422
        )
423
424 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...
425
            self,
426
            user: User,
427
            reset_password_token: str,
428
    ) -> None:
429
        """
430
        Reset password link for user
431
        :param user: user to notify
432
        :param reset_password_token: token for resetting password
433
        """
434
        logger.debug(self, 'user: {}'.format(user.user_id))
435
        logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format(
436
            user.email,
437
        ))
438
        translator = Translator(self.config, default_lang=user.lang)
439
        async_email_sender = EmailSender(
440
            self.config,
441
            self._smtp_config,
442
            self.config.EMAIL_NOTIFICATION_ACTIVATED
443
        )
444
        subject = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_SUBJECT.replace(
445
            EST.WEBSITE_TITLE,
446
            str(self.config.WEBSITE_TITLE)
447
        )
448
        message = MIMEMultipart('alternative')
449
        message['Subject'] = subject
450
        message['From'] = self._get_sender()
451
        message['To'] = formataddr((user.get_display_name(), user.email))
452
453
        text_template_file_path = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_TEXT  # nopep8
454
        html_template_file_path = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_HTML  # nopep8
455
        # TODO - G.M - 2018-08-17 - Generate token
456
        context = {
457
            'user': user,
458
            'logo_url': get_email_logo_frontend_url(self.config),
459
            'reset_password_url': get_reset_password_frontend_url(self.config, token=reset_password_token),  # nopep8
460
        }
461
        body_text = self._render_template(
462
            mako_template_filepath=text_template_file_path,
463
            context=context,
464
            translator=translator,
465
        )
466
467
        body_html = self._render_template(
468
            mako_template_filepath=html_template_file_path,
469
            context=context,
470
            translator=translator,
471
        )
472
473
        part1 = MIMEText(body_text, 'plain', 'utf-8')
474
        part2 = MIMEText(body_html, 'html', 'utf-8')
475
476
        # Attach parts into message container.
477
        # According to RFC 2046, the last part of a multipart message,
478
        # in this case the HTML message, is best and preferred.
479
        message.attach(part1)
480
        message.attach(part2)
481
482
        send_email_through(
483
            config=self.config,
484
            sendmail_callable=async_email_sender.send_mail,
485
            message=message
486
        )
487
488
    def _render_template(
489
            self,
490
            mako_template_filepath: str,
491
            context: dict,
492
            translator: Translator
493
    ) -> str:
494
        """
495
        Render mako template with all needed current variables.
496
497
        :param mako_template_filepath: file path of mako template
498
        :param context: dict with template context
499
        :return: template rendered string
500
        """
501
502
        template = Template(filename=mako_template_filepath)
503
        return template.render(
504
            _=translator.get_translation,
505
            config=self.config,
506
            **context
507
        )
508
509
    def _build_email_body_for_content(
510
            self,
511
            mako_template_filepath: str,
512
            role: UserRoleInWorkspace,
513
            content_in_context: ContentInContext,
514
            workspace_in_context: WorkspaceInContext,
515
            actor: User,
516
            translator: Translator
517
    ) -> str:
518
        """
519
        Build an email body and return it as a string
520
        :param mako_template_filepath: the absolute path to the mako template to be used for email body building
521
        :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification
522
        :param content: the content item related to the notification
523
        :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
524
        :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html
525
        """
526
        _ = translator.get_translation
527
        logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
528
        content = content_in_context.content
529
        main_title = content.label
530
        content_intro = ''
531
        content_text = ''
532
        call_to_action_text = ''
533
534
        call_to_action_url = content_in_context.frontend_url
535
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
536
        status_icon_url = ''
537
        workspace_url = workspace_in_context.frontend_url
538
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
539
        logo_url = get_email_logo_frontend_url(self.config)
540
        action = content.get_last_action().id
541
        if ActionDescription.COMMENT == action:
542
            content_intro = _('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)
543
            content_text = content.description
544
            call_to_action_text = _('Answer')
545
546
        elif ActionDescription.CREATION == action:
547
            # Default values (if not overriden)
548
            content_text = content.description
549
            call_to_action_text = _('View online')
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 = ''
577
578
            elif CONTENT_TYPES.Page.slug == content.type:
579
                content_intro = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
580
                previous_revision = content.get_previous_revision()
581
                title_diff = ''
582
                if previous_revision.label != content.label:
583
                    title_diff = htmldiff(previous_revision.label, content.label)
584
                content_text = str('<p id="content-body-intro">{}</p> {text}</p> {title_diff} {content_diff}').format(
585
                    text=_('Here is an overview of the changes:'),
586
                    title_diff=title_diff,
587
                    content_diff=htmldiff(previous_revision.description, content.description)
588
                )
589
            elif CONTENT_TYPES.Thread.slug == content.type:
590
                content_intro = _('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name)
591
                previous_revision = content.get_previous_revision()
592
                title_diff = ''
593
                if previous_revision.label != content.label:
594
                    title_diff = htmldiff(previous_revision.label, content.label)
595
                content_text = str('<p id="content-body-intro">{}</p> {text} {title_diff} {content_diff}').format(
596
                    text=_('Here is an overview of the changes:'),
597
                    title_diff=title_diff,
598
                    content_diff=htmldiff(previous_revision.description, content.description)
599
                )
600
        elif ActionDescription.EDITION == action:
601
            call_to_action_text = _('View online')
602
603
            if CONTENT_TYPES.File.slug == content.type:
604
                content_intro = _('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name)
605
                content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + \
606
                    content.description
607
608
        elif ActionDescription.STATUS_UPDATE == action:
609
            call_to_action_text = _('View online')
610
            intro_user_msg = _(
611
                '<span id="content-intro-username">{}</span> '
612
                'updated the following status:'
613
            )
614
            content_intro = intro_user_msg.format(actor.display_name)
615
            intro_body_msg = '<p id="content-body-intro">{}: {}</p>'
616
            content_text = intro_body_msg.format(
617
                content.get_label(),
618
                content.get_status().label,
619
            )
620
621
        if '' == content_intro and content_text == '':
622
            # Skip notification, but it's not normal
623
            logger.error(
624
                self, 'A notification is being sent but no content. '
625
                      'Here are some debug informations: [content_id: {cid}]'
626
                      '[action: {act}][author: {actor}]'.format(
627
                    cid=content.content_id, act=action, actor=actor
628
                )
629
            )
630
            raise ValueError('Unexpected empty notification')
631
632
        context = {
633
            'user': role.user,
634
            'workspace': role.workspace,
635
            'workspace_url': workspace_url,
636
            'main_title': main_title,
637
            'status_label': content.get_status().label,
638
            'status_icon_url': status_icon_url,
639
            'role_label': role.role_as_label(),
640
            'content_intro': content_intro,
641
            'content_text': content_text,
642
            'call_to_action_text': call_to_action_text,
643
            'call_to_action_url': call_to_action_url,
644
            'logo_url': logo_url,
645
        }
646
        user = role.user
647
        workspace = role.workspace
648
        body_content = self._render_template(
649
            mako_template_filepath=mako_template_filepath,
650
            context=context,
651
            translator=translator,
652
        )
653
        return body_content
654
655
656
def get_email_manager(config: CFG, session: Session):
657
    """
658
    :return: EmailManager instance
659
    """
660
    #  TODO: Find a way to import properly without cyclic import
661
662
    smtp_config = SmtpConfiguration(
663
        config.EMAIL_NOTIFICATION_SMTP_SERVER,
664
        config.EMAIL_NOTIFICATION_SMTP_PORT,
665
        config.EMAIL_NOTIFICATION_SMTP_USER,
666
        config.EMAIL_NOTIFICATION_SMTP_PASSWORD
667
    )
668
669
    return EmailManager(config=config, smtp_config=smtp_config, session=session)
670