Passed
Pull Request — develop (#29)
by inkhey
02:37
created

notifier.EmailNotifier.notify_content_update()   C

Complexity

Conditions 9

Size

Total Lines 69
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 43
dl 0
loc 69
rs 6.5146
c 0
b 0
f 0
cc 9
nop 2

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