Passed
Pull Request — develop (#31)
by Bastien
01:43
created

notifier.get_email_manager()   A

Complexity

Conditions 1

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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