Skip to content

Commit

Permalink
ŝanĝas la retpoŝtan provizanton al Postmark per Anymail
Browse files Browse the repository at this point in the history
adiaŭ SendGrid!
  • Loading branch information
interDist committed May 18, 2024
1 parent e22a559 commit 580ac0e
Show file tree
Hide file tree
Showing 25 changed files with 341 additions and 66 deletions.
Empty file added chat/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions chat/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.apps import AppConfig
from django.conf import settings
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _

from anymail.backends.base import AnymailBaseBackend
from anymail.message import AnymailMessage
from anymail.signals import pre_send


class ChatConfig(AppConfig):
name = "chat"
verbose_name = _("Communicator")


@receiver(pre_send)
def enrich_envelope(sender: type[AnymailBaseBackend], message: AnymailMessage, **kwargs):
if getattr(message, 'mass_mail', False):
# Mass emails must be sent via the broadcast stream.
return
if message.subject.startswith('[[CHAT]]'):
extra = getattr(message, 'esp_extra', {})
extra['MessageStream'] = 'notifications-to-users'
message.esp_extra = extra
message.tags = ['notification:chat']
message.track_opens = True
message.metadata = {'env': settings.ENVIRONMENT}
message.subject = message.subject.removeprefix('[[CHAT]]').strip()
33 changes: 33 additions & 0 deletions core/apps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import re

from django.apps import AppConfig
from django.conf import settings
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _

from anymail.backends.base import AnymailBaseBackend
from anymail.message import AnymailMessage
from anymail.signals import pre_send
from gql import Client as GQLClient, gql
from gql.transport.exceptions import TransportError, TransportQueryError
from gql.transport.requests import RequestsHTTPTransport as GQLHttpTransport
Expand Down Expand Up @@ -39,3 +45,30 @@ def ready(self):
FEEDBACK_TYPES[feedback_key] = (
feedback._replace(url=discussion['node']['url'])
)


@receiver(pre_send)
def enrich_envelope(sender: type[AnymailBaseBackend], message: AnymailMessage, **kwargs):
"""
Add extra Anymail / Postmark information to an email message, for correct
processing. This is done via a `pre_send` signal because Django sends
emails in multiple manners, and customizing each one of them individually
is quite cumbersome.
`message` can be an EmailMessage, an EmailMultiAlternatives, or a derivative.
"""
if getattr(message, 'mass_mail', False):
# Mass emails must be sent via the broadcast stream.
return
tags = {
'[[ACCOUNT]]': 'account',
'[[ACCOUNT-EMAIL]]': 'email',
'[[PLACE-DETAILS]]': 'authorized',
}
possible_prefixes_pattern = '|'.join(re.escape(prefix) for prefix in tags.keys())
if match := re.match(possible_prefixes_pattern, message.subject):
extra = getattr(message, 'esp_extra', {})
extra['MessageStream'] = 'notifications-to-users'
message.esp_extra = extra
message.tags = [f'notification:{tags[match.group()]}']
message.metadata = {'env': settings.ENVIRONMENT}
message.subject = message.subject.removeprefix(match.group()).strip()
7 changes: 6 additions & 1 deletion core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def save(self, commit=True):
context = {
'site_name': config.site_name,
'ENV': settings.ENVIRONMENT,
'RICH_ENVELOPE': getattr(settings, 'EMAIL_RICH_ENVELOPES', None),
'subject_prefix': settings.EMAIL_SUBJECT_PREFIX_FULL,
'url': url,
'url_first': url[:url.rindex('/')+1],
Expand Down Expand Up @@ -267,7 +268,11 @@ def send_mail(self,

args = [subject_template_name, email_template_name, context, *args]
kwargs.update(html_email_template_name=html_email_template_name)
context.update({'ENV': settings.ENVIRONMENT, 'subject_prefix': settings.EMAIL_SUBJECT_PREFIX_FULL})
context.update({
'ENV': settings.ENVIRONMENT,
'RICH_ENVELOPE': getattr(settings, 'EMAIL_RICH_ENVELOPES', None),
'subject_prefix': settings.EMAIL_SUBJECT_PREFIX_FULL,
})
super().send_mail(*args, **kwargs)

def save(self, **kwargs):
Expand Down
2 changes: 2 additions & 0 deletions core/templates/email/new_email_subject.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% load i18n %}{% autoescape off %}

{% if RICH_ENVELOPE %}[[ACCOUNT-EMAIL]]{% endif %}

{{ subject_prefix }}{% trans "Change of email address" context "Email subject" %}

{% endautoescape %}
2 changes: 2 additions & 0 deletions core/templates/email/old_email_subject.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% load i18n %}{% autoescape off %}

{% if RICH_ENVELOPE %}[[ACCOUNT-EMAIL]]{% endif %}

{{ subject_prefix }}{% trans "Change of email address" context "Email subject" %}

{% endautoescape %}
2 changes: 2 additions & 0 deletions core/templates/email/password_reset_subject.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% load i18n %}{% autoescape off %}

{% if RICH_ENVELOPE %}[[ACCOUNT]]{% endif %}

{{ subject_prefix }}{% trans "Password reset" context "Email subject" %}

{% endautoescape %}
6 changes: 3 additions & 3 deletions core/templates/email/snippets/preview_header.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div class="row well base-form">
<h5 class="col-xs-3">
<strong data-toggle="tooltip" title="[email protected]">Pasporta Servo</strong>
<strong data-toggle="tooltip" title="{{ mailing_address|default:"[email protected]" }}">Pasporta Servo</strong>
</h5>
<h5 class="col-xs-9 text-left">
<span id="preview_subject"></span>
<small>- <span id="preview_preheader"></span></small>
<small>&ndash; <span id="preview_preheader"></span></small>
</h5>
</div>
</div>
2 changes: 2 additions & 0 deletions core/templates/email/system-email_verify_subject.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% load i18n %}{% autoescape off %}

{% if RICH_ENVELOPE %}[[ACCOUNT-EMAIL]]{% endif %}

{{ subject_prefix }}{% trans "Is this your email address?" context "Email subject" %}

{% endautoescape %}
2 changes: 2 additions & 0 deletions core/templates/email/username_remind_subject.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% load i18n %}{% autoescape off %}

{% if RICH_ENVELOPE %}[[ACCOUNT]]{% endif %}

{{ subject_prefix }}{% trans "Username reminder" context "Email subject" %}

{% endautoescape %}
30 changes: 22 additions & 8 deletions core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import operator
import re
from functools import reduce
from typing import Optional, Sequence, Tuple

from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail import EmailMessage, get_connection
from django.core.mail.backends.base import BaseEmailBackend
from django.utils.functional import (
SimpleLazyObject, keep_lazy_text, lazy, new_method_proxy,
)
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe

import requests
from anymail.message import AnymailMessage


def getattr_(obj, path):
Expand Down Expand Up @@ -49,8 +52,12 @@ def _lazy_joiner(sep, items, item_to_string=str):
setattr(SimpleLazyObject, '__mul__', new_method_proxy(operator.mul))


def send_mass_html_mail(datatuple, fail_silently=False, user=None, password=None,
connection=None):
def send_mass_html_mail(
datatuple: Sequence[Tuple[str, str, str, Optional[str], Sequence[str] | None]],
fail_silently: bool = False,
auth_user: Optional[str] = None, auth_password: Optional[str] = None,
connection: Optional[BaseEmailBackend] = None,
) -> int:
"""
Given a datatuple of (subject, text_content, html_content, from_email,
recipient_list), sends each message to each recipient list. Returns the
Expand All @@ -62,16 +69,23 @@ def send_mass_html_mail(datatuple, fail_silently=False, user=None, password=None
If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
"""
connection = connection or get_connection(
username=user, password=password, fail_silently=fail_silently)
messages = []
username=auth_user, password=auth_password, fail_silently=fail_silently)
messages: Sequence[EmailMessage] = []
default_from = settings.DEFAULT_FROM_EMAIL
for subject, text, html, from_email, recipients in datatuple:
subject = ''.join(subject.splitlines())
recipients = [r.strip() for r in recipients]
message = EmailMultiAlternatives(
subject, text, default_from, recipients,
recipients = [r.strip() for r in recipients] if recipients else []
message = AnymailMessage(
subject, text, from_email or default_from, recipients,
headers={'Reply-To': 'Pasporta Servo <[email protected]>'})
message.attach_alternative(html, 'text/html')
# TODO: Implement custom one-click unsubscribe.
message.esp_extra = {'MessageStream': 'broadcast'}
if tag_match := re.match(r'\[\[([a-zA-Z0-9_-]+)\]\]', subject):
message.tags = [tag_match.group(1)]
message.subject = subject.removeprefix(tag_match.group()).strip()
message.merge_data = {} # Enable batch sending mode.
setattr(message, 'mass_mail', True)
messages.append(message)
return connection.send_messages(messages) or 0

Expand Down
13 changes: 11 additions & 2 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ class PasswordResetView(PasswordResetBuiltinView):
"""
This extension of Django's built-in view allows to send a different
email depending on whether the user is active (True) or no (False).
See also the companion SystemPasswordResetRequestForm.
See also the companion `SystemPasswordResetRequestForm`.
"""
html_email_template_name = {True: 'email/password_reset.html', False: 'email/password_reset_activate.html'}
email_template_name = {True: 'email/password_reset.txt', False: 'email/password_reset_activate.txt'}
Expand Down Expand Up @@ -461,6 +461,7 @@ def post(self, request, *args, **kwargs):
context = {
'site_name': config.site_name,
'ENV': settings.ENVIRONMENT,
'RICH_ENVELOPE': getattr(settings, 'EMAIL_RICH_ENVELOPES', None),
'subject_prefix': settings.EMAIL_SUBJECT_PREFIX_FULL,
'url': url,
'url_first': url[:url.rindex('/')+1],
Expand Down Expand Up @@ -781,11 +782,19 @@ class MassMailView(AuthMixin, generic.FormView):
form_class = MassMailForm
display_permission_denied = False
exact_role = AuthRole.ADMIN
# Keep the email address separate from the one used for transactional
# emails, for better email sender reputation.
mailing_address = '[email protected]'

def dispatch(self, request, *args, **kwargs):
kwargs['auth_base'] = None
return super().dispatch(request, *args, **kwargs)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['mailing_address'] = self.mailing_address
return context

def get_success_url(self):
return format_lazy(
'{success_url}?nb={sent}',
Expand All @@ -800,7 +809,7 @@ def form_valid(self, form):
preheader = form.cleaned_data['preheader']
heading = form.cleaned_data['heading']
category = form.cleaned_data['categories']
default_from = settings.DEFAULT_FROM_EMAIL
default_from = f'Pasporta Servo <{self.mailing_address}>'
template = get_template('email/mass_email.html')

opening = make_aware(datetime(2014, 11, 24))
Expand Down
2 changes: 2 additions & 0 deletions hosting/templates/email/new_authorization_subject.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% load i18n %}{% autoescape off %}

{% if RICH_ENVELOPE %}[[PLACE-DETAILS]]{% endif %}

{{ subject_prefix }}{% trans "You received an Authorization" context "Email subject" %}

{% endautoescape %}
1 change: 1 addition & 0 deletions hosting/views/places.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ def send_email(self, user, place):
email_context = {
'site_name': config.site_name,
'ENV': settings.ENVIRONMENT,
'RICH_ENVELOPE': getattr(settings, 'EMAIL_RICH_ENVELOPES', None),
'subject_prefix': settings.EMAIL_SUBJECT_PREFIX_FULL,
'user': user,
'place': place,
Expand Down
4 changes: 4 additions & 0 deletions locale/eo/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ msgstr "Aboni"
msgid "yes,no"
msgstr "jes,ne"

#: chat/apps.py
msgid "Communicator"
msgstr "Komunikilo"

#: core/admin/admin.py core/models.py core/templates/core/base.html
#: core/views.py hosting/admin/admin.py
#: hosting/templates/hosting/phone_form.html
Expand Down
13 changes: 13 additions & 0 deletions pasportaservo/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def get_env_setting(setting):
'django.contrib.gis',

'fontawesomefree',
'anymail',
'compressor',
'crispy_forms',
'django_extensions',
Expand All @@ -84,6 +85,7 @@ def get_env_setting(setting):

'blog',
'book',
'chat',
'core',
'hosting',
'links',
Expand Down Expand Up @@ -199,6 +201,17 @@ def get_env_setting(setting):
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
TEST_EMAIL_BACKENDS = {
'dummy': {
'EMAIL_BACKEND': 'anymail.backends.test.EmailBackend',
'EMAIL_RICH_ENVELOPES': True,
},
'live': {
'EMAIL_BACKEND': 'anymail.backends.postmark.EmailBackend',
'POSTMARK_SERVER_TOKEN': 'POSTMARK_API_TEST',
'EMAIL_RICH_ENVELOPES': True,
}
}

# Internationalization
# https://docs.djangoproject.com/en/stable/topics/i18n/
Expand Down
1 change: 1 addition & 0 deletions pasportaservo/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
EMAIL_HOST = '127.0.0.1'
EMAIL_PORT = '1025'
INTERNAL_IPS = ('127.0.0.1',)
ANYMAIL_DEBUG_API_REQUESTS = True

EMAIL_SUBJECT_PREFIX = '[PS test] '
EMAIL_SUBJECT_PREFIX_FULL = '[Pasporta Servo][{}] '.format(ENVIRONMENT)
Expand Down
5 changes: 3 additions & 2 deletions pasportaservo/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
}
}

EMAIL_BACKEND = "sgbackend.SendGridBackend"
SENDGRID_API_KEY = get_env_setting('SENDGRID_API_KEY')
EMAIL_BACKEND = 'anymail.backends.postmark.EmailBackend'
POSTMARK_SERVER_TOKEN = get_env_setting('POSTMARK_SERVER_TOKEN')
EMAIL_RICH_ENVELOPES = True
EMAIL_SUBJECT_PREFIX = '[PS] '
EMAIL_SUBJECT_PREFIX_FULL = '[Pasporta Servo] '

Expand Down
5 changes: 3 additions & 2 deletions pasportaservo/settings/staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
}
}

EMAIL_BACKEND = "sgbackend.SendGridBackend"
SENDGRID_API_KEY = get_env_setting('SENDGRID_API_KEY')
EMAIL_BACKEND = 'anymail.backends.postmark.EmailBackend'
POSTMARK_SERVER_TOKEN = get_env_setting('POSTMARK_SERVER_TOKEN')
EMAIL_RICH_ENVELOPES = True
EMAIL_SUBJECT_PREFIX = '[PS ido] '
EMAIL_SUBJECT_PREFIX_FULL = '[Pasporta Servo][{}] '.format(ENVIRONMENT)

Expand Down
1 change: 1 addition & 0 deletions pasportaservo/templates/postman/email_user_subject.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% load i18n utils %}
[[CHAT]]
{% filter compact %}
{% autoescape off %}
{% blocktrans with object.obfuscated_sender as sender and object.subject as subject trimmed %}
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Pillow==10.3.0
awesome-slugify==1.6.5
commonmark==0.9.1
csscompressor==0.9.5
django-anymail[postmark]==10.3
django-braces==1.14.0
djangocodemirror==2.1.0
django-compressor==2.4
Expand Down Expand Up @@ -34,7 +35,6 @@ pymemcache==3.5.2
phonenumberslite==8.12.19
requests==2.27.1
rstr==3.0.0
sendgrid-django==4.2.0
sentry-sdk>=1.18
user_agents==2.2.0

Expand Down
Loading

0 comments on commit 580ac0e

Please sign in to comment.