Completed
Push — develop ( 10efb0...b51ddf )
by Bastien
16s queued 14s
created

notifier.EmailManager.__init__()   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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