1import logging
2
3from django.conf import settings
4from django.contrib.auth import get_user_model
5from django.core.mail import get_connection
6from django.core.mail.message import EmailMultiAlternatives
7from django.template.loader import render_to_string
8from django.utils.translation import override
9
10from wagtail.admin.auth import users_with_page_permission
11from wagtail.core.models import GroupApprovalTask, TaskState, WorkflowState
12from wagtail.core.utils import camelcase_to_underscore
13from wagtail.users.models import UserProfile
14
15
16logger = logging.getLogger('wagtail.admin')
17
18
19class OpenedConnection:
20    """Context manager for mail connections to ensure they are closed when manually opened"""
21    def __init__(self, connection):
22        self.connection = connection
23
24    def __enter__(self):
25        self.connection.open()
26        return self.connection
27
28    def __exit__(self, type, value, traceback):
29        self.connection.close()
30        return self.connection
31
32
33def send_mail(subject, message, recipient_list, from_email=None, **kwargs):
34    """
35    Wrapper around Django's EmailMultiAlternatives as done in send_mail().
36    Custom from_email handling and special Auto-Submitted header.
37    """
38    if not from_email:
39        if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
40            from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
41        elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
42            from_email = settings.DEFAULT_FROM_EMAIL
43        else:
44            from_email = 'webmaster@localhost'
45
46    connection = kwargs.get('connection', False) or get_connection(
47        username=kwargs.get('auth_user', None),
48        password=kwargs.get('auth_password', None),
49        fail_silently=kwargs.get('fail_silently', None),
50    )
51    multi_alt_kwargs = {
52        'connection': connection,
53        'headers': {
54            'Auto-Submitted': 'auto-generated',
55        }
56    }
57    mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, **multi_alt_kwargs)
58    html_message = kwargs.get('html_message', None)
59    if html_message:
60        mail.attach_alternative(html_message, 'text/html')
61
62    return mail.send()
63
64
65def send_moderation_notification(revision, notification, excluded_user=None):
66    # Get list of recipients
67    if notification == 'submitted':
68        # Get list of publishers
69        include_superusers = getattr(settings, 'WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS', True)
70        recipient_users = users_with_page_permission(revision.page, 'publish', include_superusers)
71    elif notification in ['rejected', 'approved']:
72        # Get submitter
73        recipient_users = [revision.user]
74    else:
75        return False
76
77    if excluded_user:
78        recipient_users = [user for user in recipient_users if user != excluded_user]
79
80    return send_notification(recipient_users, notification, {'revision': revision})
81
82
83def send_notification(recipient_users, notification, extra_context):
84    # Get list of email addresses
85    email_recipients = [
86        recipient for recipient in recipient_users
87        if recipient.email and getattr(
88            UserProfile.get_for_user(recipient),
89            notification + '_notifications'
90        )
91    ]
92
93    # Return if there are no email addresses
94    if not email_recipients:
95        return True
96
97    # Get template
98    template_subject = 'wagtailadmin/notifications/' + notification + '_subject.txt'
99    template_text = 'wagtailadmin/notifications/' + notification + '.txt'
100    template_html = 'wagtailadmin/notifications/' + notification + '.html'
101
102    # Common context to template
103    context = {
104        "settings": settings,
105    }
106    context.update(extra_context)
107
108    connection = get_connection()
109
110    with OpenedConnection(connection) as open_connection:
111
112        # Send emails
113        sent_count = 0
114        for recipient in email_recipients:
115            try:
116                # update context with this recipient
117                context["user"] = recipient
118
119                # Translate text to the recipient language settings
120                with override(recipient.wagtail_userprofile.get_preferred_language()):
121                    # Get email subject and content
122                    email_subject = render_to_string(template_subject, context).strip()
123                    email_content = render_to_string(template_text, context).strip()
124
125                kwargs = {}
126                if getattr(settings, 'WAGTAILADMIN_NOTIFICATION_USE_HTML', False):
127                    kwargs['html_message'] = render_to_string(template_html, context)
128
129                # Send email
130                send_mail(email_subject, email_content, [recipient.email], connection=open_connection, **kwargs)
131                sent_count += 1
132            except Exception:
133                logger.exception(
134                    "Failed to send notification email '%s' to %s",
135                    email_subject, recipient.email
136                )
137
138    return sent_count == len(email_recipients)
139
140
141class Notifier:
142    """Generic class for sending event notifications: callable, intended to be connected to a signal to send
143    notifications using rendered templates. """
144
145    notification = ''
146    template_directory = 'wagtailadmin/notifications/'
147
148    def __init__(self, valid_classes):
149        # the classes of the calling instance that the notifier can handle
150        self.valid_classes = valid_classes
151
152    def can_handle(self, instance, **kwargs):
153        """Returns True if the Notifier can handle sending the notification from the instance, otherwise False"""
154        return isinstance(instance, self.valid_classes)
155
156    def get_valid_recipients(self, instance, **kwargs):
157        """Returns a set of the final list of recipients for the notification message"""
158        return set()
159
160    def get_template_base_prefix(self, instance, **kwargs):
161        return camelcase_to_underscore(type(instance).__name__) + '_'
162
163    def get_context(self, instance, **kwargs):
164        return {'settings': settings}
165
166    def get_template_set(self, instance, **kwargs):
167        """Return a dictionary of template paths for the templates: by default, a text message"""
168        template_base = self.get_template_base_prefix(instance) + self.notification
169
170        template_text = self.template_directory + template_base + '.txt'
171
172        return {
173            'text': template_text,
174        }
175
176    def send_notifications(self, template_set, context, recipients, **kwargs):
177        raise NotImplementedError
178
179    def __call__(self, instance=None, **kwargs):
180        """Send notifications from an instance (intended to be the signal sender), returning True if all sent correctly
181        and False otherwise"""
182
183        if not self.can_handle(instance, **kwargs):
184            return False
185
186        recipients = self.get_valid_recipients(instance, **kwargs)
187
188        if not recipients:
189            return True
190
191        template_set = self.get_template_set(instance, **kwargs)
192
193        context = self.get_context(instance, **kwargs)
194
195        return self.send_notifications(template_set, context, recipients, **kwargs)
196
197
198class EmailNotificationMixin:
199    """Mixin for sending email notifications upon events"""
200
201    def get_recipient_users(self, instance, **kwargs):
202        """Gets the ideal set of recipient users, without accounting for notification preferences or missing email addresses"""
203
204        return set()
205
206    def get_valid_recipients(self, instance, **kwargs):
207        """Filters notification recipients to those allowing the notification type on their UserProfile, and those
208        with an email address"""
209
210        return {recipient for recipient in self.get_recipient_users(instance, **kwargs) if recipient.email and getattr(
211            UserProfile.get_for_user(recipient),
212            self.notification + '_notifications'
213        )}
214
215    def get_template_set(self, instance, **kwargs):
216        """Return a dictionary of template paths for the templates for the email subject and the text and html
217        alternatives"""
218        template_base = self.get_template_base_prefix(instance) + self.notification
219
220        template_subject = self.template_directory + template_base + '_subject.txt'
221        template_text = self.template_directory + template_base + '.txt'
222        template_html = self.template_directory + template_base + '.html'
223
224        return {
225            'subject': template_subject,
226            'text': template_text,
227            'html': template_html,
228        }
229
230    def send_emails(self, template_set, context, recipients, **kwargs):
231
232        connection = get_connection()
233        sent_count = 0
234        try:
235            with OpenedConnection(connection) as open_connection:
236
237                # Send emails
238                for recipient in recipients:
239                    try:
240
241                        # update context with this recipient
242                        context["user"] = recipient
243
244                        # Translate text to the recipient language settings
245                        with override(recipient.wagtail_userprofile.get_preferred_language()):
246                            # Get email subject and content
247                            email_subject = render_to_string(template_set['subject'], context).strip()
248                            email_content = render_to_string(template_set['text'], context).strip()
249
250                        kwargs = {}
251                        if getattr(settings, 'WAGTAILADMIN_NOTIFICATION_USE_HTML', False):
252                            kwargs['html_message'] = render_to_string(template_set['html'], context)
253
254                        # Send email
255                        send_mail(email_subject, email_content, [recipient.email], connection=open_connection, **kwargs)
256                        sent_count += 1
257                    except Exception:
258                        logger.exception(
259                            "Failed to send notification email '%s' to %s",
260                            email_subject, recipient.email
261                        )
262        except (TimeoutError, ConnectionError):
263            logger.exception("Mail connection error, notification sending skipped")
264
265        return sent_count == len(recipients)
266
267    def send_notifications(self, template_set, context, recipients, **kwargs):
268        return self.send_emails(template_set, context, recipients, **kwargs)
269
270
271class BaseWorkflowStateEmailNotifier(EmailNotificationMixin, Notifier):
272    """A base notifier to send email updates for WorkflowState events"""
273
274    def __init__(self):
275        super().__init__((WorkflowState,))
276
277    def get_context(self, workflow_state, **kwargs):
278        context = super().get_context(workflow_state, **kwargs)
279        context['page'] = workflow_state.page
280        context['workflow'] = workflow_state.workflow
281        return context
282
283
284class WorkflowStateApprovalEmailNotifier(BaseWorkflowStateEmailNotifier):
285    """A notifier to send email updates for WorkflowState approval events"""
286
287    notification = 'approved'
288
289    def get_recipient_users(self, workflow_state, **kwargs):
290        triggering_user = kwargs.get('user', None)
291        recipients = {}
292        requested_by = workflow_state.requested_by
293        if requested_by != triggering_user:
294            recipients = {requested_by}
295
296        return recipients
297
298
299class WorkflowStateRejectionEmailNotifier(BaseWorkflowStateEmailNotifier):
300    """A notifier to send email updates for WorkflowState rejection events"""
301
302    notification = 'rejected'
303
304    def get_recipient_users(self, workflow_state, **kwargs):
305        triggering_user = kwargs.get('user', None)
306        recipients = {}
307        requested_by = workflow_state.requested_by
308        if requested_by != triggering_user:
309            recipients = {requested_by}
310
311        return recipients
312
313    def get_context(self, workflow_state, **kwargs):
314        context = super().get_context(workflow_state, **kwargs)
315        task_state = workflow_state.current_task_state.specific
316        context['task'] = task_state.task
317        context['task_state'] = task_state
318        context['comment'] = task_state.get_comment()
319        return context
320
321
322class WorkflowStateSubmissionEmailNotifier(BaseWorkflowStateEmailNotifier):
323    """A notifier to send email updates for WorkflowState submission events"""
324
325    notification = 'submitted'
326
327    def get_recipient_users(self, workflow_state, **kwargs):
328        triggering_user = kwargs.get('user', None)
329        recipients = get_user_model().objects.none()
330        include_superusers = getattr(settings, 'WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS', True)
331        if include_superusers:
332            recipients = get_user_model().objects.filter(is_superuser=True)
333        if triggering_user:
334            recipients.exclude(pk=triggering_user.pk)
335
336        return recipients
337
338    def get_context(self, workflow_state, **kwargs):
339        context = super().get_context(workflow_state, **kwargs)
340        context['requested_by'] = workflow_state.requested_by
341        return context
342
343
344class BaseGroupApprovalTaskStateEmailNotifier(EmailNotificationMixin, Notifier):
345    """A base notifier to send email updates for GroupApprovalTask events"""
346
347    def __init__(self):
348        super().__init__((TaskState,))
349
350    def can_handle(self, instance, **kwargs):
351        if super().can_handle(instance, **kwargs) and isinstance(instance.task.specific, GroupApprovalTask):
352            return True
353        return False
354
355    def get_context(self, task_state, **kwargs):
356        context = super().get_context(task_state, **kwargs)
357        context['page'] = task_state.workflow_state.page
358        context['task'] = task_state.task.specific
359        return context
360
361    def get_recipient_users(self, task_state, **kwargs):
362        triggering_user = kwargs.get('user', None)
363
364        group_members = get_user_model().objects.filter(groups__in=task_state.task.specific.groups.all())
365
366        recipients = group_members
367
368        include_superusers = getattr(settings, 'WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS', True)
369        if include_superusers:
370            superusers = get_user_model().objects.filter(is_superuser=True)
371            recipients = recipients | superusers
372
373        if triggering_user:
374            recipients = recipients.exclude(pk=triggering_user.pk)
375
376        return recipients
377
378
379class GroupApprovalTaskStateSubmissionEmailNotifier(BaseGroupApprovalTaskStateEmailNotifier):
380    """A notifier to send email updates for GroupApprovalTask submission events"""
381
382    notification = 'submitted'
383