wbmailing 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbmailing might be problematic. Click here for more details.
- wbmailing/__init__.py +1 -0
- wbmailing/admin.py +74 -0
- wbmailing/apps.py +14 -0
- wbmailing/backend.py +131 -0
- wbmailing/celery.py +0 -0
- wbmailing/dynamic_preferences_registry.py +35 -0
- wbmailing/factories.py +211 -0
- wbmailing/filters/__init__.py +8 -0
- wbmailing/filters/mailing_lists.py +84 -0
- wbmailing/filters/mails.py +74 -0
- wbmailing/management/__init__.py +22 -0
- wbmailing/migrations/0001_initial_squashed_squashed_0008_alter_mail_bcc_email_alter_mail_cc_email_and_more.py +649 -0
- wbmailing/migrations/0002_delete_mailingsettings.py +16 -0
- wbmailing/migrations/0003_alter_mailinglistsubscriberchangerequest_options.py +25 -0
- wbmailing/migrations/__init__.py +0 -0
- wbmailing/models/__init__.py +6 -0
- wbmailing/models/mailing_lists.py +386 -0
- wbmailing/models/mails.py +895 -0
- wbmailing/serializers/__init__.py +19 -0
- wbmailing/serializers/mailing_lists.py +209 -0
- wbmailing/serializers/mails.py +251 -0
- wbmailing/tasks.py +37 -0
- wbmailing/templatetags/__init__.py +0 -0
- wbmailing/templatetags/mailing_tags.py +22 -0
- wbmailing/tests/__init__.py +0 -0
- wbmailing/tests/conftest.py +30 -0
- wbmailing/tests/models/__init__.py +0 -0
- wbmailing/tests/models/test_mailing_lists.py +297 -0
- wbmailing/tests/models/test_mails.py +205 -0
- wbmailing/tests/signals.py +124 -0
- wbmailing/tests/test_serializers.py +28 -0
- wbmailing/tests/test_tasks.py +49 -0
- wbmailing/tests/test_viewsets.py +216 -0
- wbmailing/tests/tests.py +142 -0
- wbmailing/urls.py +90 -0
- wbmailing/viewsets/__init__.py +32 -0
- wbmailing/viewsets/analytics.py +110 -0
- wbmailing/viewsets/buttons/__init__.py +10 -0
- wbmailing/viewsets/buttons/mailing_lists.py +91 -0
- wbmailing/viewsets/buttons/mails.py +98 -0
- wbmailing/viewsets/display/__init__.py +16 -0
- wbmailing/viewsets/display/mailing_lists.py +175 -0
- wbmailing/viewsets/display/mails.py +318 -0
- wbmailing/viewsets/endpoints/__init__.py +8 -0
- wbmailing/viewsets/endpoints/mailing_lists.py +86 -0
- wbmailing/viewsets/endpoints/mails.py +51 -0
- wbmailing/viewsets/mailing_lists.py +320 -0
- wbmailing/viewsets/mails.py +425 -0
- wbmailing/viewsets/menu/__init__.py +5 -0
- wbmailing/viewsets/menu/mailing_lists.py +37 -0
- wbmailing/viewsets/menu/mails.py +25 -0
- wbmailing/viewsets/titles/__init__.py +17 -0
- wbmailing/viewsets/titles/mailing_lists.py +63 -0
- wbmailing/viewsets/titles/mails.py +55 -0
- wbmailing-2.2.1.dist-info/METADATA +5 -0
- wbmailing-2.2.1.dist-info/RECORD +57 -0
- wbmailing-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
from anymail.exceptions import AnymailRecipientsRefused
|
|
5
|
+
from anymail.signals import tracking
|
|
6
|
+
from celery import shared_task
|
|
7
|
+
from django.apps import apps
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.contrib.postgres.fields import ArrayField
|
|
10
|
+
from django.core.files.base import ContentFile
|
|
11
|
+
from django.core.mail import EmailMultiAlternatives
|
|
12
|
+
from django.db import models
|
|
13
|
+
from django.db.models import Count, OuterRef, Subquery
|
|
14
|
+
from django.dispatch import receiver
|
|
15
|
+
from django.template import Context, Template
|
|
16
|
+
from django.utils import timezone
|
|
17
|
+
from django.utils.html import strip_tags
|
|
18
|
+
from django.utils.translation import gettext
|
|
19
|
+
from django.utils.translation import gettext_lazy as _
|
|
20
|
+
from django.utils.translation import pgettext_lazy
|
|
21
|
+
from django_fsm import FSMField, transition
|
|
22
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
23
|
+
from psycopg.types.range import TimestamptzRange
|
|
24
|
+
from rest_framework.reverse import reverse
|
|
25
|
+
from sentry_sdk import capture_message
|
|
26
|
+
from wbcore.contrib.color.enums import WBColor
|
|
27
|
+
from wbcore.contrib.directory.models import EmailContact
|
|
28
|
+
from wbcore.contrib.documents.models import Document, DocumentType
|
|
29
|
+
from wbcore.contrib.documents.models.mixins import DocumentMixin
|
|
30
|
+
from wbcore.contrib.icons import WBIcon
|
|
31
|
+
from wbcore.enums import RequestType
|
|
32
|
+
from wbcore.markdown.template import resolve_markdown
|
|
33
|
+
from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
|
|
34
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
35
|
+
create_simple_display,
|
|
36
|
+
)
|
|
37
|
+
from wbcore.models import WBModel
|
|
38
|
+
from wbcore.utils.html import convert_html2text
|
|
39
|
+
|
|
40
|
+
from .mailing_lists import MailingListEmailContactThroughModel
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def can_administrate_mail(mail, user):
|
|
44
|
+
return user.has_perm("wbmailing.can_administrate_mail")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MassMail(DocumentMixin, WBModel):
|
|
48
|
+
class Status(models.TextChoices):
|
|
49
|
+
DRAFT = "DRAFT", _("Draft")
|
|
50
|
+
PENDING = "PENDING", _("Pending")
|
|
51
|
+
SENT = "SENT", _("Sent")
|
|
52
|
+
SEND_LATER = "SEND LATER", _("Send later")
|
|
53
|
+
DENIED = "DENIED", _("Denied")
|
|
54
|
+
|
|
55
|
+
class Meta:
|
|
56
|
+
verbose_name = _("Mass Mail")
|
|
57
|
+
verbose_name_plural = _("Mass Mails")
|
|
58
|
+
permissions = (("can_administrate_mail", "Can administrate mail"),)
|
|
59
|
+
|
|
60
|
+
status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name=_("Status"))
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def subquery_expected_mails(cls, massmail_name="pk"):
|
|
64
|
+
"""
|
|
65
|
+
Create subquery to count expected mails
|
|
66
|
+
|
|
67
|
+
Arguments:
|
|
68
|
+
massmail_name {str} -- Outerref field
|
|
69
|
+
"""
|
|
70
|
+
return Subquery(
|
|
71
|
+
EmailContact.objects.filter(subscriptions__mails=OuterRef(massmail_name))
|
|
72
|
+
.values("subscriptions__mails")
|
|
73
|
+
.annotate(c=Count("subscriptions__mails"))
|
|
74
|
+
.values("c")[:1],
|
|
75
|
+
output_field=models.IntegerField(),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
BUTTON_SUBMIT = ActionButton(
|
|
79
|
+
method=RequestType.PATCH,
|
|
80
|
+
identifiers=("wbmailing:massmail",),
|
|
81
|
+
icon=WBIcon.SEND.icon,
|
|
82
|
+
color=ButtonDefaultColor.WARNING,
|
|
83
|
+
key="submit",
|
|
84
|
+
label=pgettext_lazy("Massmail draft", "Submit"),
|
|
85
|
+
action_label=_("Submitting"),
|
|
86
|
+
description_fields=_("<p>Subject: {{subject}}</p>"),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@transition(
|
|
90
|
+
field=status,
|
|
91
|
+
source=[Status.DRAFT],
|
|
92
|
+
target=Status.PENDING,
|
|
93
|
+
custom={"_transition_button": BUTTON_SUBMIT},
|
|
94
|
+
)
|
|
95
|
+
def submit(self, by=None, description=None, **kwargs):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
BUTTON_DENIED = ActionButton(
|
|
99
|
+
method=RequestType.PATCH,
|
|
100
|
+
identifiers=("wbmailing:massmail",),
|
|
101
|
+
icon=WBIcon.REJECT.icon,
|
|
102
|
+
color=ButtonDefaultColor.ERROR,
|
|
103
|
+
key="deby",
|
|
104
|
+
label=_("Deny"),
|
|
105
|
+
action_label=_("Denial"),
|
|
106
|
+
description_fields=_("<p>Subject: {{subject}}</p>"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@transition(
|
|
110
|
+
field=status,
|
|
111
|
+
source=[Status.PENDING],
|
|
112
|
+
target=Status.DENIED,
|
|
113
|
+
permission=can_administrate_mail,
|
|
114
|
+
custom={"_transition_button": BUTTON_DENIED},
|
|
115
|
+
)
|
|
116
|
+
def deny(self, by=None, description=None, **kwargs):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
BUTTON_REVISE = ActionButton(
|
|
120
|
+
method=RequestType.PATCH,
|
|
121
|
+
identifiers=("wbmailing:massmail",),
|
|
122
|
+
icon=WBIcon.EDIT.icon,
|
|
123
|
+
color=ButtonDefaultColor.WARNING,
|
|
124
|
+
key="revise",
|
|
125
|
+
label=_("Revise"),
|
|
126
|
+
action_label=_("Revision"),
|
|
127
|
+
description_fields=_("<p>Subject: {{subject}}</p>"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@transition(
|
|
131
|
+
field=status,
|
|
132
|
+
source=[Status.PENDING],
|
|
133
|
+
target=Status.DRAFT,
|
|
134
|
+
permission=can_administrate_mail,
|
|
135
|
+
custom={"_transition_button": BUTTON_REVISE},
|
|
136
|
+
)
|
|
137
|
+
def revise(self, by=None, description=None, **kwargs):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
BUTTON_SEND = ActionButton(
|
|
141
|
+
method=RequestType.PATCH,
|
|
142
|
+
identifiers=("wbmailing:massmail",),
|
|
143
|
+
icon=WBIcon.MAIL.icon,
|
|
144
|
+
color=ButtonDefaultColor.SUCCESS,
|
|
145
|
+
key="send",
|
|
146
|
+
label=_("Send"),
|
|
147
|
+
action_label=_("Sending"),
|
|
148
|
+
description_fields=_("<p>Subject: {{subject}}</p><p>Mailing Lists: {{mailing_lists}}</p>"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@transition(
|
|
152
|
+
field=status,
|
|
153
|
+
source=[Status.PENDING, Status.SEND_LATER],
|
|
154
|
+
target=Status.SENT,
|
|
155
|
+
permission=lambda instance, user: user.has_perm("wbmailing.can_administrate_mail") and not instance.send_at,
|
|
156
|
+
custom={"_transition_button": BUTTON_SEND},
|
|
157
|
+
)
|
|
158
|
+
def send(self, by=None, description=None, **kwargs):
|
|
159
|
+
send_mail_task.delay(self.id)
|
|
160
|
+
|
|
161
|
+
BUTTON_SEND_LATER = ActionButton(
|
|
162
|
+
method=RequestType.PATCH,
|
|
163
|
+
identifiers=("wbmailing:massmail",),
|
|
164
|
+
icon=WBIcon.SEND_LATER.icon,
|
|
165
|
+
key="sendlater",
|
|
166
|
+
label=_("Send Later"),
|
|
167
|
+
action_label=_("Sending later"),
|
|
168
|
+
description_fields=_("<p>Subject: {{subject}}</p><p>Mailing Lists: {{mailing_lists}}</p>"),
|
|
169
|
+
instance_display=create_simple_display([["send_at"]]),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@transition(
|
|
173
|
+
field=status,
|
|
174
|
+
source=[Status.PENDING, Status.SENT, Status.SEND_LATER],
|
|
175
|
+
target=Status.SEND_LATER,
|
|
176
|
+
permission=lambda instance, user: user.has_perm("wbmailing.can_administrate_mail")
|
|
177
|
+
and (not instance.send_at or instance.send_at > timezone.now()),
|
|
178
|
+
custom={"_transition_button": BUTTON_SEND_LATER},
|
|
179
|
+
)
|
|
180
|
+
def sendlater(self, by=None, description=None, **kwargs):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_emails(cls, included_mailing_list_ids: list[int], excluded_mailing_list_ids: list[int] = None):
|
|
185
|
+
included_emails_id = MailingListEmailContactThroughModel.objects.filter(
|
|
186
|
+
status=MailingListEmailContactThroughModel.Status.SUBSCRIBED, mailing_list__in=included_mailing_list_ids
|
|
187
|
+
).values_list("email_contact", flat=True)
|
|
188
|
+
|
|
189
|
+
excluded_addresses = (
|
|
190
|
+
MailingListEmailContactThroughModel.objects.filter(
|
|
191
|
+
status=MailingListEmailContactThroughModel.Status.SUBSCRIBED,
|
|
192
|
+
mailing_list__in=excluded_mailing_list_ids,
|
|
193
|
+
).values_list("email_contact__address", flat=True)
|
|
194
|
+
if excluded_mailing_list_ids
|
|
195
|
+
else []
|
|
196
|
+
)
|
|
197
|
+
return (
|
|
198
|
+
EmailContact.objects.filter(id__in=included_emails_id)
|
|
199
|
+
.exclude(address__in=list(excluded_addresses))
|
|
200
|
+
.distinct("address")
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_mail_addresses(self):
|
|
204
|
+
return self.get_emails(
|
|
205
|
+
self.mailing_lists.values_list("id"), self.excluded_mailing_lists.values_list("id")
|
|
206
|
+
).values("address")
|
|
207
|
+
|
|
208
|
+
def create_email(self, email):
|
|
209
|
+
global_preferences = global_preferences_registry.manager()
|
|
210
|
+
context = MassMail.get_context(email, self.subject, self.attachment_url)
|
|
211
|
+
if self.documents and self.documents.exists():
|
|
212
|
+
context.update(
|
|
213
|
+
{"urls": [attachment.generate_shareable_link().link for attachment in self.documents.all()]}
|
|
214
|
+
)
|
|
215
|
+
rendered_subject = Template(self.subject).render(Context(context))
|
|
216
|
+
body = resolve_markdown(self.body, extensions=["sane_lists"])
|
|
217
|
+
if self.template:
|
|
218
|
+
rendered_body = self.template.render_content(body, extra_context=context)
|
|
219
|
+
else:
|
|
220
|
+
rendered_body = Template(body).render(Context(context))
|
|
221
|
+
|
|
222
|
+
from_mail = self.from_email or global_preferences["wbmailing__default_source_mail"]
|
|
223
|
+
|
|
224
|
+
return Mail.get_mailmessage(
|
|
225
|
+
rendered_subject, rendered_body, from_mail, [email], attachments=self.documents, mass_mail=self
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def send_test_mail(self, email):
|
|
229
|
+
"""
|
|
230
|
+
Send a test mail
|
|
231
|
+
|
|
232
|
+
Arguments:
|
|
233
|
+
email {str} -- The address to send the test mail to
|
|
234
|
+
"""
|
|
235
|
+
self.subject = _("Test mail: {}").format(self.subject)
|
|
236
|
+
|
|
237
|
+
msg = self.create_email(email)
|
|
238
|
+
msg.send()
|
|
239
|
+
|
|
240
|
+
from_email = models.EmailField(null=True, blank=True, verbose_name=_("From"))
|
|
241
|
+
template = models.ForeignKey(
|
|
242
|
+
"MailTemplate",
|
|
243
|
+
related_name="mass_mails",
|
|
244
|
+
null=True,
|
|
245
|
+
blank=True,
|
|
246
|
+
on_delete=models.SET_NULL,
|
|
247
|
+
verbose_name=_("Template"),
|
|
248
|
+
)
|
|
249
|
+
mailing_lists = models.ManyToManyField(
|
|
250
|
+
"MailingList",
|
|
251
|
+
related_name="mails",
|
|
252
|
+
verbose_name=_("Mailing Lists"),
|
|
253
|
+
help_text=_("The mailing lists to extract emails from. Duplicates will be skipped."),
|
|
254
|
+
)
|
|
255
|
+
excluded_mailing_lists = models.ManyToManyField(
|
|
256
|
+
"MailingList",
|
|
257
|
+
related_name="excluded_mails",
|
|
258
|
+
verbose_name=_("Excluded Mailing Lists"),
|
|
259
|
+
help_text=_(
|
|
260
|
+
"The mailing lists to exlude emails from. The resulting list of emails is equals to Mailing Lists - Excluded Mailing Lists "
|
|
261
|
+
),
|
|
262
|
+
blank=True,
|
|
263
|
+
)
|
|
264
|
+
subject = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Subject"))
|
|
265
|
+
body = models.TextField(default="", verbose_name=_("Body"))
|
|
266
|
+
|
|
267
|
+
attachment_url = models.URLField(null=True, blank=True, verbose_name=_("Attachment (URL)"))
|
|
268
|
+
|
|
269
|
+
body_json = models.JSONField(default=dict, null=True, blank=True)
|
|
270
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
271
|
+
creator = models.ForeignKey(
|
|
272
|
+
"directory.Person",
|
|
273
|
+
related_name="created_mails",
|
|
274
|
+
on_delete=models.CASCADE,
|
|
275
|
+
blank=True,
|
|
276
|
+
null=True,
|
|
277
|
+
verbose_name=_("Creator"),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
send_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Send At"))
|
|
281
|
+
|
|
282
|
+
def __str__(self):
|
|
283
|
+
return self.subject
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def get_endpoint_basename(cls) -> str:
|
|
287
|
+
return "wbmailing:massmail"
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def get_representation_endpoint(cls):
|
|
291
|
+
return "wbmailing:massmailrepresentation-list"
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def get_representation_value_key(cls):
|
|
295
|
+
return "id"
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def get_representation_label_key(cls):
|
|
299
|
+
return "{{subject}}"
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def get_context(cls, email, subject=None, attachment_url=None):
|
|
303
|
+
emails = EmailContact.objects.filter(address=email)
|
|
304
|
+
context = {"email": email}
|
|
305
|
+
if subject:
|
|
306
|
+
context["subject"] = subject
|
|
307
|
+
if attachment_url:
|
|
308
|
+
context["attachment_url"] = attachment_url
|
|
309
|
+
if emails.exists():
|
|
310
|
+
email_contact = emails.first()
|
|
311
|
+
unsubscribe_url = f'{settings.BASE_ENDPOINT_URL}{reverse("wbmailing:manage_mailing_list_subscriptions", args=[email_contact.id])}'
|
|
312
|
+
context["unsubscribe"] = f"<a href={unsubscribe_url}>" + _("Unsubscribe</a>")
|
|
313
|
+
entry = email_contact.entry
|
|
314
|
+
if entry:
|
|
315
|
+
if salutation := entry.salutation:
|
|
316
|
+
context["salutation"] = salutation
|
|
317
|
+
casted_entry = entry.get_casted_entry()
|
|
318
|
+
if hasattr(casted_entry, "first_name"):
|
|
319
|
+
context["first_name"] = casted_entry.first_name
|
|
320
|
+
if hasattr(casted_entry, "last_name"):
|
|
321
|
+
context["last_name"] = casted_entry.last_name
|
|
322
|
+
return context
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class Mail(DocumentMixin, WBModel):
|
|
326
|
+
class Status(models.TextChoices):
|
|
327
|
+
OPENED = "OPENED", _("Opened")
|
|
328
|
+
DELIVERED = "DELIVERED", _("Delivered")
|
|
329
|
+
BOUNCED = "BOUNCED", _("Bounced")
|
|
330
|
+
OTHER = "OTHER", _("Other")
|
|
331
|
+
|
|
332
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
333
|
+
last_send = models.DateTimeField(verbose_name=_("Last Sent"), default=timezone.now)
|
|
334
|
+
template = models.ForeignKey(
|
|
335
|
+
"MailTemplate",
|
|
336
|
+
related_name="mails",
|
|
337
|
+
null=True,
|
|
338
|
+
blank=True,
|
|
339
|
+
on_delete=models.SET_NULL,
|
|
340
|
+
verbose_name=_("Template"),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
message_ids = ArrayField(models.CharField(max_length=255, null=True, blank=True), default=list)
|
|
344
|
+
mass_mail = models.ForeignKey(
|
|
345
|
+
"MassMail", related_name="mails", null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Mass Mail")
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
from_email = models.EmailField(verbose_name=_("From"))
|
|
349
|
+
to_email = models.ManyToManyField("directory.EmailContact", related_name="mail_to", verbose_name=_("To"))
|
|
350
|
+
cc_email = models.ManyToManyField(
|
|
351
|
+
"directory.EmailContact", related_name="mail_cc", blank=True, verbose_name=_("CC")
|
|
352
|
+
)
|
|
353
|
+
bcc_email = models.ManyToManyField(
|
|
354
|
+
"directory.EmailContact", related_name="mail_bcc", blank=True, verbose_name=_("BCC")
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
subject = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Subject"))
|
|
358
|
+
body = models.TextField(blank=True, null=True, verbose_name=_("body"))
|
|
359
|
+
body_json = models.JSONField(default=dict, null=True, blank=True)
|
|
360
|
+
|
|
361
|
+
def resend(self):
|
|
362
|
+
"""
|
|
363
|
+
Resend that mail
|
|
364
|
+
"""
|
|
365
|
+
context = {}
|
|
366
|
+
if self.to_email.count() == 1:
|
|
367
|
+
context = MassMail.get_context(self.to_email.first().address, self.subject)
|
|
368
|
+
if self.documents.exists():
|
|
369
|
+
context.update(
|
|
370
|
+
{"urls": [attachment.generate_shareable_link().link for attachment in self.documents.all()]}
|
|
371
|
+
)
|
|
372
|
+
rendered_subject = Template(self.subject).render(Context(context))
|
|
373
|
+
|
|
374
|
+
if self.mass_mail:
|
|
375
|
+
body = resolve_markdown(self.mass_mail.body, extensions=["sane_lists"])
|
|
376
|
+
template = self.mass_mail.template
|
|
377
|
+
if self.mass_mail.template:
|
|
378
|
+
rendered_body = template.render_content(body, extra_context=context)
|
|
379
|
+
else:
|
|
380
|
+
rendered_body = Template(body).render(Context(context))
|
|
381
|
+
else:
|
|
382
|
+
rendered_body = self.body
|
|
383
|
+
|
|
384
|
+
msg = {
|
|
385
|
+
"subject": rendered_subject,
|
|
386
|
+
"body": rendered_body,
|
|
387
|
+
"from_email": self.from_email,
|
|
388
|
+
"to": list(self.to_email.values_list("address", flat=True)),
|
|
389
|
+
"bcc": list(self.bcc_email.values_list("address", flat=True)),
|
|
390
|
+
"cc": list(self.cc_email.values_list("address", flat=True)),
|
|
391
|
+
"mail_id": self.id,
|
|
392
|
+
}
|
|
393
|
+
if self.mass_mail:
|
|
394
|
+
msg["mass_mail_id"] = self.mass_mail.id
|
|
395
|
+
send_mail_as_task.delay(**msg)
|
|
396
|
+
|
|
397
|
+
def convert_files_to_documents(self, attachments, alternatives):
|
|
398
|
+
# If DMS sends this email, we expect to find an alternative containing a encoded dictionary with the
|
|
399
|
+
# necessary information to retreive the document object
|
|
400
|
+
dms_alternatives = list(
|
|
401
|
+
map(
|
|
402
|
+
lambda x: json.loads(x[0].decode("ascii")),
|
|
403
|
+
filter(lambda x: x[1] == "wbdms/document", alternatives), # leave this mimetype. Used by wbcore
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
document_type, created = DocumentType.objects.get_or_create(name="mailing")
|
|
408
|
+
for name, payload, mimetype in attachments:
|
|
409
|
+
# If an alternative sent from DMS is found, we match against the attachment email.
|
|
410
|
+
dms_elements = list(filter(lambda x: x["filename"] == name, dms_alternatives))
|
|
411
|
+
if (len(dms_elements) == 1) and (document_id := dms_elements[0].get("id", None)):
|
|
412
|
+
document = Document.objects.get(id=document_id)
|
|
413
|
+
# Otherwise, we update or create the corresponding Document objects based on its generated system_key
|
|
414
|
+
else:
|
|
415
|
+
content_file = ContentFile(payload, name=name)
|
|
416
|
+
system_key_base = f"mail-{self.id}" if not self.mass_mail else f"massmail-{self.mass_mail.id}"
|
|
417
|
+
document, created = Document.objects.update_or_create(
|
|
418
|
+
document_type=document_type,
|
|
419
|
+
system_created=True,
|
|
420
|
+
system_key=f"{system_key_base}-{name}",
|
|
421
|
+
defaults={"file": content_file},
|
|
422
|
+
)
|
|
423
|
+
document.link(self)
|
|
424
|
+
|
|
425
|
+
class Meta:
|
|
426
|
+
verbose_name = _("Mail")
|
|
427
|
+
verbose_name_plural = _("Mails")
|
|
428
|
+
|
|
429
|
+
def __str__(self):
|
|
430
|
+
return self.subject or str(self.id)
|
|
431
|
+
|
|
432
|
+
@classmethod
|
|
433
|
+
def get_mailmessage(
|
|
434
|
+
cls,
|
|
435
|
+
rendered_subject,
|
|
436
|
+
rendered_body,
|
|
437
|
+
from_email,
|
|
438
|
+
to,
|
|
439
|
+
bcc=None,
|
|
440
|
+
cc=None,
|
|
441
|
+
attachments=None,
|
|
442
|
+
mass_mail=None,
|
|
443
|
+
mail=None,
|
|
444
|
+
):
|
|
445
|
+
"""
|
|
446
|
+
Get a set of parameters and returns the custom mail message alternative
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
rendered_subject (str): The mail subject
|
|
450
|
+
rendered_body (str): Mail Body
|
|
451
|
+
from_email (str): from email address
|
|
452
|
+
to (list<string>): List of destination addresses
|
|
453
|
+
bcc (list<string>): List of BCC addressses
|
|
454
|
+
cc (list<string>): List of CC addresses
|
|
455
|
+
attachments (list<File>): List of File to attach to the mail
|
|
456
|
+
mass_mail (MassMail): The originating mass mail, None if a direct mail
|
|
457
|
+
mail (Mail): The Mail object in case of a resend
|
|
458
|
+
"""
|
|
459
|
+
msg = EmailMultiAlternatives(
|
|
460
|
+
strip_tags(rendered_subject), convert_html2text(rendered_body), from_email, to=to, cc=cc, bcc=bcc
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
msg.mass_mail = mass_mail
|
|
464
|
+
msg.mail = mail
|
|
465
|
+
msg.attach_alternative(rendered_body, "text/html")
|
|
466
|
+
|
|
467
|
+
if attachments:
|
|
468
|
+
for attachment in attachments:
|
|
469
|
+
msg.attach(attachment.file.name, attachment.file.read())
|
|
470
|
+
return msg
|
|
471
|
+
|
|
472
|
+
@classmethod
|
|
473
|
+
def create_mail_from_mailmessage(cls, msg, user=None):
|
|
474
|
+
def get_or_create_emails(emails):
|
|
475
|
+
for email in emails:
|
|
476
|
+
if e := EmailContact.objects.filter(address=email).first():
|
|
477
|
+
yield e
|
|
478
|
+
else:
|
|
479
|
+
yield EmailContact.objects.create(address=email)
|
|
480
|
+
|
|
481
|
+
args = {}
|
|
482
|
+
|
|
483
|
+
if mass_mail := getattr(msg, "mass_mail", None):
|
|
484
|
+
args["mass_mail"] = mass_mail
|
|
485
|
+
args["subject"] = mass_mail.subject
|
|
486
|
+
else:
|
|
487
|
+
args["subject"] = msg.subject
|
|
488
|
+
args["body"] = msg.body
|
|
489
|
+
for content, mimetype in msg.alternatives:
|
|
490
|
+
if mimetype == "text/html":
|
|
491
|
+
args["body"] = content
|
|
492
|
+
|
|
493
|
+
mail = Mail.objects.create(from_email=msg.from_email, **args)
|
|
494
|
+
|
|
495
|
+
if msg.attachments:
|
|
496
|
+
mail.convert_files_to_documents(msg.attachments, msg.alternatives)
|
|
497
|
+
if hasattr(msg, "clear_attachments"):
|
|
498
|
+
msg.attachments = []
|
|
499
|
+
if msg.to:
|
|
500
|
+
_email = get_or_create_emails(msg.to)
|
|
501
|
+
if _email:
|
|
502
|
+
mail.to_email.set(_email)
|
|
503
|
+
if msg.cc:
|
|
504
|
+
_email = get_or_create_emails(msg.cc)
|
|
505
|
+
if _email:
|
|
506
|
+
mail.cc_email.set(_email)
|
|
507
|
+
if msg.bcc:
|
|
508
|
+
_email = get_or_create_emails(msg.bcc)
|
|
509
|
+
if _email:
|
|
510
|
+
mail.bcc_email.set(_email)
|
|
511
|
+
mail.save()
|
|
512
|
+
return mail
|
|
513
|
+
|
|
514
|
+
@classmethod
|
|
515
|
+
def subquery_send_mails(cls, mass_mail_name="pk"):
|
|
516
|
+
"""
|
|
517
|
+
Create subquery to count number of mails created from sent Mass Mail
|
|
518
|
+
"""
|
|
519
|
+
return Subquery(
|
|
520
|
+
cls.objects.filter(mass_mail=OuterRef(mass_mail_name))
|
|
521
|
+
.values("mass_mail")
|
|
522
|
+
.annotate(c=Count("mass_mail"))
|
|
523
|
+
.values("c")[:1],
|
|
524
|
+
output_field=models.IntegerField(),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
@classmethod
|
|
528
|
+
def get_endpoint_basename(cls):
|
|
529
|
+
return "wbmailing:mail"
|
|
530
|
+
|
|
531
|
+
@classmethod
|
|
532
|
+
def get_representation_endpoint(cls):
|
|
533
|
+
return "wbmailing:mailrepresentation-list"
|
|
534
|
+
|
|
535
|
+
@classmethod
|
|
536
|
+
def get_representation_value_key(cls):
|
|
537
|
+
return "id"
|
|
538
|
+
|
|
539
|
+
@classmethod
|
|
540
|
+
def get_representation_label_key(cls):
|
|
541
|
+
return "{{subject}}"
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class MailEvent(models.Model):
|
|
545
|
+
class EventType(models.TextChoices):
|
|
546
|
+
"""Constants for normalized Anymail event types"""
|
|
547
|
+
|
|
548
|
+
CREATED = "CREATED", _("Created") # Default Type
|
|
549
|
+
QUEUED = "QUEUED", _("Queued") # the ESP has accepted the message and will try to send it (possibly later)
|
|
550
|
+
SENT = "SENT", _("Sent") # the ESP has sent the message (though it may or may not get delivered)
|
|
551
|
+
RESENT = "RESENT", _("Resent")
|
|
552
|
+
REJECTED = (
|
|
553
|
+
"REJECTED",
|
|
554
|
+
_("Rejected"),
|
|
555
|
+
) # the ESP refused to send the messsage (e.g., suppression list, policy, invalid email)
|
|
556
|
+
FAILED = "FAILED", _("Failed") # the ESP was unable to send the message (e.g., template rendering error)
|
|
557
|
+
BOUNCED = "BOUNCED", _("Bounced") # rejected or blocked by receiving MTA
|
|
558
|
+
DEFERRED = (
|
|
559
|
+
"DEFERRED",
|
|
560
|
+
_("Deferred"),
|
|
561
|
+
) # delayed by receiving MTA; should be followed by a later BOUNCED or DELIVERED
|
|
562
|
+
DELIVERED = "DELIVERED", _("Delivered") # accepted by receiving MTA
|
|
563
|
+
AUTORESPONDED = "AUTORESPONDED", _("Autoresponded") # a bot replied
|
|
564
|
+
OPENED = "OPENED", _("Opened") # open tracking
|
|
565
|
+
CLICKED = "CLICKED", _("Clicked") # click tracking
|
|
566
|
+
COMPLAINED = "COMPLAINED", _("Complained") # recipient reported as spam (e.g., through feedback loop)
|
|
567
|
+
UNSUBSCRIBED = "UNSUBSCRIBED", _("Unsubscribed") # recipient attempted to unsubscribe
|
|
568
|
+
SUBSCRIBED = "SUBSCRIBED", _("Subscribed") # signed up for mailing list through ESP-hosted form
|
|
569
|
+
INBOUND = "INBOUND", _("Inbound") # received message
|
|
570
|
+
INBOUND_FAILED = "INBOUND_FAILED", _("Inbound Failed")
|
|
571
|
+
UNKNOWN = "UNKNOWN", _("Unknown") # anything else
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def get_color(cls, name):
|
|
575
|
+
return {
|
|
576
|
+
"CREATED": WBColor.GREY.value,
|
|
577
|
+
"QUEUED": WBColor.BLUE.value,
|
|
578
|
+
"SENT": WBColor.BLUE_LIGHT.value,
|
|
579
|
+
"RESENT": WBColor.BLUE_DARK.value,
|
|
580
|
+
"REJECTED": WBColor.RED_DARK.value,
|
|
581
|
+
"FAILED": WBColor.RED_DARK.value,
|
|
582
|
+
"BOUNCED": WBColor.RED.value,
|
|
583
|
+
"DEFERRED": WBColor.RED_LIGHT.value,
|
|
584
|
+
"DELIVERED": WBColor.YELLOW_LIGHT.value,
|
|
585
|
+
"AUTORESPONDED": WBColor.YELLOW.value,
|
|
586
|
+
"OPENED": WBColor.GREEN_LIGHT.value,
|
|
587
|
+
"CLICKED": WBColor.GREEN.value,
|
|
588
|
+
"COMPLAINED": WBColor.RED_DARK.value,
|
|
589
|
+
"UNSUBSCRIBED": WBColor.RED_DARK.value,
|
|
590
|
+
"SUBSCRIBED": WBColor.BLUE_DARK.value,
|
|
591
|
+
"INBOUND": WBColor.YELLOW.value,
|
|
592
|
+
"INBOUND_FAILED": WBColor.RED_DARK.value,
|
|
593
|
+
"UNKNOWN": WBColor.YELLOW_DARK.value,
|
|
594
|
+
}[name]
|
|
595
|
+
|
|
596
|
+
class RejectReason(models.TextChoices):
|
|
597
|
+
"""Constants for normalized Anymail reject/drop reasons"""
|
|
598
|
+
|
|
599
|
+
INVALID = "INVALID", _("invalid") # bad address format
|
|
600
|
+
BOUNCED = "BOUNCED", _("bounced") # (previous) bounce from recipient
|
|
601
|
+
TIMED_OUT = "TIMED_OUT", _("timed_out") # (previous) repeated failed delivery attempts
|
|
602
|
+
BLOCKED = "BLOCKED", _("blocked") # ESP policy suppression
|
|
603
|
+
SPAM = "SPAM", _("spam") # (previous) spam complaint from recipient
|
|
604
|
+
UNSUBSCRIBED = "UNSUBSCRIBED", _("unsubscribed") # (previous) unsubscribe request from recipient
|
|
605
|
+
OTHER = "OTHER", _("other")
|
|
606
|
+
|
|
607
|
+
mail = models.ForeignKey("Mail", related_name="events", on_delete=models.CASCADE, verbose_name=_("Mail"))
|
|
608
|
+
timestamp = models.DateTimeField(default=timezone.now, verbose_name=_("Datetime"))
|
|
609
|
+
event_type = models.CharField(
|
|
610
|
+
max_length=64, default=EventType.CREATED, choices=EventType.choices, verbose_name=_("Type")
|
|
611
|
+
)
|
|
612
|
+
reject_reason = models.CharField(
|
|
613
|
+
max_length=64, null=True, blank=True, choices=RejectReason.choices, verbose_name=_("Rejection Reason")
|
|
614
|
+
)
|
|
615
|
+
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
|
|
616
|
+
recipient = models.EmailField(null=True, blank=True, verbose_name=_("Recipient"))
|
|
617
|
+
click_url = models.URLField(
|
|
618
|
+
max_length=2048, null=True, blank=True, verbose_name=_("Clicked URL")
|
|
619
|
+
) # 2048 is the maximum allowed url size by browser.
|
|
620
|
+
ip = models.CharField(max_length=126, null=True, blank=True, verbose_name=_("IP Used To Send Mail"))
|
|
621
|
+
user_agent = models.TextField(null=True, blank=True)
|
|
622
|
+
raw_data = models.JSONField(default=dict, null=True, blank=True, verbose_name=_("Raw Data"))
|
|
623
|
+
metadata = models.JSONField(default=dict, null=True, blank=True, verbose_name=_("Metadata"))
|
|
624
|
+
tags = ArrayField(models.CharField(max_length=64, null=True, blank=True), default=list)
|
|
625
|
+
|
|
626
|
+
class Meta:
|
|
627
|
+
verbose_name = _("Mail Event")
|
|
628
|
+
verbose_name_plural = _("Mail Events")
|
|
629
|
+
|
|
630
|
+
@classmethod
|
|
631
|
+
def subquery_delivered_mails(cls, mass_mail_name="pk"):
|
|
632
|
+
"""
|
|
633
|
+
Create subquery to count number of delivered mails
|
|
634
|
+
"""
|
|
635
|
+
return Subquery(
|
|
636
|
+
cls.objects.filter(event_type=MailEvent.EventType.DELIVERED, mail__mass_mail=OuterRef(mass_mail_name))
|
|
637
|
+
.values("mail__mass_mail")
|
|
638
|
+
.annotate(c=Count("mail__mass_mail"))
|
|
639
|
+
.values("c")[:1],
|
|
640
|
+
output_field=models.IntegerField(),
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
@classmethod
|
|
644
|
+
def subquery_opened_mails(cls, mass_mail_name="pk"):
|
|
645
|
+
"""
|
|
646
|
+
Create subquery to count number of opened mails
|
|
647
|
+
"""
|
|
648
|
+
return Subquery(
|
|
649
|
+
cls.objects.filter(event_type=MailEvent.EventType.OPENED, mail__mass_mail=OuterRef(mass_mail_name))
|
|
650
|
+
.values("mail__mass_mail")
|
|
651
|
+
.annotate(c=Count("mail__to_email", distinct=True))
|
|
652
|
+
.values("c")[:1],
|
|
653
|
+
output_field=models.IntegerField(),
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
@classmethod
|
|
657
|
+
def subquery_clicked_mails(cls, mass_mail_name="pk"):
|
|
658
|
+
"""
|
|
659
|
+
Create subquery to count number of clicked mails
|
|
660
|
+
"""
|
|
661
|
+
return Subquery(
|
|
662
|
+
cls.objects.filter(event_type=MailEvent.EventType.CLICKED, mail__mass_mail=OuterRef(mass_mail_name))
|
|
663
|
+
.values("mail__mass_mail")
|
|
664
|
+
.annotate(c=Count("mail__mass_mail"))
|
|
665
|
+
.values("c")[:1],
|
|
666
|
+
output_field=models.IntegerField(),
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
@classmethod
|
|
670
|
+
def get_endpoint_basename(cls):
|
|
671
|
+
return "wbmailing:mailevent"
|
|
672
|
+
|
|
673
|
+
@classmethod
|
|
674
|
+
def get_representation_value_key(cls):
|
|
675
|
+
return "id"
|
|
676
|
+
|
|
677
|
+
@classmethod
|
|
678
|
+
def get_representation_label_key(cls):
|
|
679
|
+
return "{{event_type}} ({{mail__subject}})"
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class MailTemplate(WBModel):
|
|
683
|
+
title = models.CharField(max_length=255)
|
|
684
|
+
template = models.TextField()
|
|
685
|
+
|
|
686
|
+
def render_content(self, content, extra_context=None):
|
|
687
|
+
context = {"content": content}
|
|
688
|
+
if extra_context:
|
|
689
|
+
context = {**context, **extra_context}
|
|
690
|
+
return Template(self.template).render(Context(context))
|
|
691
|
+
|
|
692
|
+
class Meta:
|
|
693
|
+
verbose_name = _("Mail Template")
|
|
694
|
+
verbose_name_plural = _("Mail Templates")
|
|
695
|
+
|
|
696
|
+
def __str__(self):
|
|
697
|
+
return self.title
|
|
698
|
+
|
|
699
|
+
@classmethod
|
|
700
|
+
def get_endpoint_basename(cls):
|
|
701
|
+
return "wbmailing:mailtemplate"
|
|
702
|
+
|
|
703
|
+
@classmethod
|
|
704
|
+
def get_representation_endpoint(cls):
|
|
705
|
+
return "wbmailing:mailtemplaterepresentation-list"
|
|
706
|
+
|
|
707
|
+
@classmethod
|
|
708
|
+
def get_representation_value_key(cls):
|
|
709
|
+
return "id"
|
|
710
|
+
|
|
711
|
+
@classmethod
|
|
712
|
+
def get_representation_label_key(cls):
|
|
713
|
+
return "{{title}}"
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@receiver(tracking)
|
|
717
|
+
def handle_mail_tracking(sender, event, esp_name, **kwargs):
|
|
718
|
+
"""
|
|
719
|
+
Signal triggered by the Sendgrid sdk. Create MailEvent
|
|
720
|
+
"""
|
|
721
|
+
global_preferences = global_preferences_registry.manager()
|
|
722
|
+
|
|
723
|
+
def handle_subscription_change(
|
|
724
|
+
recipient, event_type, description, mass_mail, automatically_approve=False, reject_reason=None
|
|
725
|
+
):
|
|
726
|
+
email_contacts = EmailContact.objects.filter(address=recipient)
|
|
727
|
+
for email_contact in email_contacts:
|
|
728
|
+
for mailing_list in mass_mail.mailing_lists.all():
|
|
729
|
+
if event_type == MailEvent.EventType.SUBSCRIBED:
|
|
730
|
+
mailing_list.subscribe(
|
|
731
|
+
email_contact,
|
|
732
|
+
reason=gettext(
|
|
733
|
+
"Received an ESP resubscription request for {}. Please check the pending subscription change request"
|
|
734
|
+
).format(recipient),
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
mailing_list.unsubscribe(
|
|
738
|
+
email_contact,
|
|
739
|
+
reason=gettext(
|
|
740
|
+
"Received an ESP unsubscription request for {}:\n <p>Event Type: {}</p>\n<p>Rejection Reason: {}</p>\n<p>Description: {}</p>\n<p>Automatically approve: {}</p>"
|
|
741
|
+
).format(recipient, event_type, reject_reason, description, automatically_approve),
|
|
742
|
+
automatically_approve=automatically_approve,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def get_mail(message_id, tags=[]):
|
|
746
|
+
mail = Mail.objects.filter(message_ids__contains=[event.message_id]).first()
|
|
747
|
+
if mail:
|
|
748
|
+
return mail
|
|
749
|
+
|
|
750
|
+
# We support only one tag
|
|
751
|
+
tags = tags[0].split("-") if tags and isinstance(tags[0], str) else []
|
|
752
|
+
if len(tags) == 2:
|
|
753
|
+
try:
|
|
754
|
+
_id = int(tags[1])
|
|
755
|
+
if tags[0] == "massmail":
|
|
756
|
+
mail = Mail.objects.filter(mass_mail=_id, to_email__address=event.recipient).first()
|
|
757
|
+
elif tags[0] == "mail":
|
|
758
|
+
mail = Mail.objects.get(id=_id)
|
|
759
|
+
except ValueError:
|
|
760
|
+
pass
|
|
761
|
+
return mail
|
|
762
|
+
|
|
763
|
+
event_type = event.event_type.upper()
|
|
764
|
+
if event_type not in MailEvent.EventType.names:
|
|
765
|
+
event_type = MailEvent.EventType.UNKNOWN
|
|
766
|
+
|
|
767
|
+
reject_reason = None
|
|
768
|
+
if event.reject_reason:
|
|
769
|
+
reject_reason = event.reject_reason.upper()
|
|
770
|
+
if reject_reason not in MailEvent.RejectReason.names:
|
|
771
|
+
reject_reason = MailEvent.RejectReason.OTHER
|
|
772
|
+
|
|
773
|
+
if event_type == MailEvent.EventType.UNKNOWN and event.esp_event.get("RecordType", None) == "SubscriptionChange":
|
|
774
|
+
if event.esp_event.get("SuppressSending", True):
|
|
775
|
+
event_type = MailEvent.EventType.UNSUBSCRIBED
|
|
776
|
+
if reason := event.esp_event.get("SuppressionReason", None):
|
|
777
|
+
if reason == "HardBounce":
|
|
778
|
+
reject_reason = MailEvent.RejectReason.BOUNCED
|
|
779
|
+
elif reason == "SpamComplaint":
|
|
780
|
+
reject_reason = MailEvent.RejectReason.SPAM
|
|
781
|
+
else:
|
|
782
|
+
reject_reason = MailEvent.RejectReason.UNSUBSCRIBED
|
|
783
|
+
else:
|
|
784
|
+
event_type = MailEvent.EventType.SUBSCRIBED
|
|
785
|
+
|
|
786
|
+
mail = get_mail(event.message_id, event.tags)
|
|
787
|
+
if mail:
|
|
788
|
+
MailEvent.objects.create(
|
|
789
|
+
mail=mail,
|
|
790
|
+
timestamp=event.timestamp if event.timestamp else timezone.now(),
|
|
791
|
+
event_type=event_type,
|
|
792
|
+
recipient=event.recipient,
|
|
793
|
+
reject_reason=reject_reason,
|
|
794
|
+
user_agent=event.user_agent,
|
|
795
|
+
click_url=event.click_url,
|
|
796
|
+
description=event.description,
|
|
797
|
+
raw_data=event.esp_event,
|
|
798
|
+
tags=event.tags,
|
|
799
|
+
)
|
|
800
|
+
is_hard_bounce = event.esp_event.get("Type", None) == "HardBounce"
|
|
801
|
+
if mail.mass_mail: # we handle unsubscription and subscription only if the mail comes from a mass mail
|
|
802
|
+
# Handle PostMark unsubcription notification
|
|
803
|
+
if event_type in [
|
|
804
|
+
MailEvent.EventType.REJECTED,
|
|
805
|
+
MailEvent.EventType.COMPLAINED,
|
|
806
|
+
MailEvent.EventType.UNSUBSCRIBED,
|
|
807
|
+
MailEvent.EventType.SUBSCRIBED,
|
|
808
|
+
] or (event_type == MailEvent.EventType.BOUNCED and is_hard_bounce):
|
|
809
|
+
handle_subscription_change(
|
|
810
|
+
event.recipient,
|
|
811
|
+
event_type,
|
|
812
|
+
event.description,
|
|
813
|
+
mail.mass_mail,
|
|
814
|
+
automatically_approve=is_hard_bounce
|
|
815
|
+
and global_preferences["wbmailing__automatically_approve_unsubscription_request_from_hard_bounce"],
|
|
816
|
+
reject_reason=reject_reason,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
else:
|
|
820
|
+
capture_message(gettext("Received event but could not find related mail"))
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
@shared_task
|
|
824
|
+
def send_mail_as_task(
|
|
825
|
+
subject=None,
|
|
826
|
+
body=None,
|
|
827
|
+
from_email=None,
|
|
828
|
+
to=None,
|
|
829
|
+
bcc=None,
|
|
830
|
+
cc=None,
|
|
831
|
+
mass_mail_id=None,
|
|
832
|
+
mail_id=None,
|
|
833
|
+
):
|
|
834
|
+
msg = Mail.get_mailmessage(
|
|
835
|
+
subject,
|
|
836
|
+
body,
|
|
837
|
+
from_email,
|
|
838
|
+
to,
|
|
839
|
+
bcc=bcc,
|
|
840
|
+
cc=cc,
|
|
841
|
+
mass_mail=MassMail.objects.get(id=mass_mail_id) if mass_mail_id else None,
|
|
842
|
+
mail=Mail.objects.get(id=mail_id) if mail_id else None,
|
|
843
|
+
)
|
|
844
|
+
msg.send()
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@shared_task
|
|
848
|
+
def send_mass_mail_as_task(mass_mail_id: int, email_address: str):
|
|
849
|
+
mass_mail = MassMail.objects.get(id=mass_mail_id)
|
|
850
|
+
try:
|
|
851
|
+
msg = mass_mail.create_email(email_address)
|
|
852
|
+
msg.send()
|
|
853
|
+
except AnymailRecipientsRefused:
|
|
854
|
+
global_preferences = global_preferences_registry.manager()
|
|
855
|
+
for mailing_list in mass_mail.mailing_lists.exclude(id__in=mass_mail.excluded_mailing_lists.values("id")):
|
|
856
|
+
for rel in MailingListEmailContactThroughModel.objects.filter(
|
|
857
|
+
mailing_list=mailing_list,
|
|
858
|
+
email_contact__address=email_address,
|
|
859
|
+
status=MailingListEmailContactThroughModel.Status.SUBSCRIBED,
|
|
860
|
+
):
|
|
861
|
+
rel.change_state(
|
|
862
|
+
automatically_approve=global_preferences[
|
|
863
|
+
"wbmailing__automatically_approve_unsubscription_request_from_hard_bounce"
|
|
864
|
+
],
|
|
865
|
+
reason="Email address was rejected by our ESP.",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
@shared_task
|
|
870
|
+
def send_mail_task(mass_mail_id):
|
|
871
|
+
"""
|
|
872
|
+
MailingListSubscriberChangeRequest post_save signal: Automatically approve if user is superuser/manager
|
|
873
|
+
|
|
874
|
+
Arguments:
|
|
875
|
+
mass_mail {MassMail} -- MassMail to send mails from
|
|
876
|
+
"""
|
|
877
|
+
mass_mail = MassMail.objects.filter(id=mass_mail_id).first()
|
|
878
|
+
|
|
879
|
+
if apps.is_installed("wbcrm.Activity"): # Until we find something else
|
|
880
|
+
from wbcrm.models import Activity, ActivityType
|
|
881
|
+
|
|
882
|
+
activity_type, created = ActivityType.objects.get_or_create(slugify_title="email", defaults={"title": "Email"})
|
|
883
|
+
Activity.objects.create(
|
|
884
|
+
status=Activity.Status.REVIEWED,
|
|
885
|
+
type=activity_type,
|
|
886
|
+
title=_("Mass mail sent: {}").format(mass_mail.subject),
|
|
887
|
+
description=mass_mail.body,
|
|
888
|
+
period=TimestamptzRange(timezone.now(), timezone.now() + timedelta(minutes=1)),
|
|
889
|
+
creator=mass_mail.creator,
|
|
890
|
+
assigned_to=mass_mail.creator,
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
mass_mail.mails.all().delete()
|
|
894
|
+
for subscriber in mass_mail.get_mail_addresses():
|
|
895
|
+
send_mass_mail_as_task.delay(mass_mail.id, subscriber["address"])
|