瀏覽代碼

feat(api): add management command to send emails

Peter Thomassen 3 年之前
父節點
當前提交
d09e7bd75d
共有 2 個文件被更改,包括 67 次插入3 次删除
  1. 63 0
      api/desecapi/management/commands/outreach-email.py
  2. 4 3
      api/desecapi/models.py

+ 63 - 0
api/desecapi/management/commands/outreach-email.py

@@ -0,0 +1,63 @@
+import argparse
+import sys
+
+from django.core.exceptions import ImproperlyConfigured
+from django.core.management import BaseCommand
+from django.template import engines
+from django.template.backends.django import DjangoTemplates
+from django.urls import resolve, reverse
+
+from desecapi.models import User
+
+
+def _get_default_template_backend():
+    # Ad-hoc implementation of https://github.com/django/django/pull/15944
+    for backend in engines.all():
+        if isinstance(backend, DjangoTemplates):
+            return backend
+    raise ImproperlyConfigured("No DjangoTemplates backend is configured.")
+
+
+class Command(BaseCommand):
+    help = 'Reach out to users with an email. Takes email template on stdin.'
+
+    def add_arguments(self, parser):
+        parser.add_argument('email', nargs='*', help='User(s) to contact, identified by their email addresses. '
+                            'Defaults to everyone with outreach_preference = True.')
+        parser.add_argument('--contentfile', nargs='?', type=argparse.FileType('r'), default=sys.stdin,
+                            help='File to take email content from. Defaults to stdin.')
+        parser.add_argument('--reason', nargs='?', default='change-outreach-preference',
+                            help='Kind of message to send. Choose from reasons given in serializers.py. Defaults to '
+                                 'newsletter with unsubscribe link (reason: change-outreach-preference).')
+        parser.add_argument('--subject', nargs='?', default=None, help='Subject, default according to "reason".')
+
+    def handle(self, *args, **options):
+        reason = options['reason']
+        path = reverse(f'v1:confirm-{reason}', args=['code'])
+        serializer_class = resolve(path).func.cls.serializer_class
+
+        content = options['contentfile'].read().strip()
+        if not content and options['contentfile'].name != '/dev/null':
+            raise RuntimeError('Empty content only allowed from /dev/null')
+
+        try:
+            subject = '[deSEC] ' + options['subject']
+        except TypeError:
+            subject = None
+
+        base_file = f'emails/{reason}/content.txt'
+        template_code = ('{%% extends "%s" %%}' % base_file)
+        if content:
+            template_code += '{% block content %}' + content + '{% endblock %}'
+        template = _get_default_template_backend().from_string(template_code)
+
+        if options['email']:
+            users = User.objects.filter(email__in=options['email'])
+        elif content:
+            users = User.objects.filter(outreach_preference=True)
+        else:
+            raise RuntimeError('To send default content, specify recipients explicitly.')
+
+        for user in users:
+            action = serializer_class.Meta.model(user=user)
+            serializer_class(action).save(subject=subject, template=template)

+ 4 - 3
api/desecapi/models.py

@@ -161,7 +161,7 @@ class User(ExportModelOperationsMixin('User'), AbstractBaseUser):
         logger.warning(f'User {pk} deleted')
         return ret
 
-    def send_email(self, reason, context=None, recipient=None):
+    def send_email(self, reason, context=None, recipient=None, subject=None, template=None):
         fast_lane = 'email_fast_lane'
         slow_lane = 'email_slow_lane'
         immediate_lane = 'email_immediate_lane'
@@ -181,12 +181,13 @@ class User(ExportModelOperationsMixin('User'), AbstractBaseUser):
             raise ValueError(f'Cannot send email to user {self.pk} without a good reason: {reason}')
 
         context = context or {}
-        content = get_template(f'emails/{reason}/content.txt').render(context)
+        template = template or get_template(f'emails/{reason}/content.txt')
+        content = template.render(context)
         content += f'\nSupport Reference: user_id = {self.pk}\n'
 
         logger.warning(f'Queuing email for user account {self.pk} (reason: {reason}, lane: {lanes[reason]})')
         num_queued = EmailMessage(
-            subject=get_template(f'emails/{reason}/subject.txt').render(context).strip(),
+            subject=(subject or get_template(f'emails/{reason}/subject.txt').render(context)).strip(),
             body=content,
             from_email=get_template('emails/from.txt').render(),
             to=[recipient or self.email],