wbmailing 2.2.1__tar.gz
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-2.2.1/.gitignore +181 -0
- wbmailing-2.2.1/PKG-INFO +5 -0
- wbmailing-2.2.1/pyproject.toml +29 -0
- wbmailing-2.2.1/wbmailing/__init__.py +1 -0
- wbmailing-2.2.1/wbmailing/admin.py +74 -0
- wbmailing-2.2.1/wbmailing/apps.py +14 -0
- wbmailing-2.2.1/wbmailing/backend.py +131 -0
- wbmailing-2.2.1/wbmailing/celery.py +0 -0
- wbmailing-2.2.1/wbmailing/dynamic_preferences_registry.py +35 -0
- wbmailing-2.2.1/wbmailing/factories.py +211 -0
- wbmailing-2.2.1/wbmailing/filters/__init__.py +8 -0
- wbmailing-2.2.1/wbmailing/filters/mailing_lists.py +84 -0
- wbmailing-2.2.1/wbmailing/filters/mails.py +74 -0
- wbmailing-2.2.1/wbmailing/locale/de/LC_MESSAGES/django.po +1110 -0
- wbmailing-2.2.1/wbmailing/management/__init__.py +22 -0
- wbmailing-2.2.1/wbmailing/migrations/0001_initial_squashed_squashed_0008_alter_mail_bcc_email_alter_mail_cc_email_and_more.py +649 -0
- wbmailing-2.2.1/wbmailing/migrations/0002_delete_mailingsettings.py +16 -0
- wbmailing-2.2.1/wbmailing/migrations/0003_alter_mailinglistsubscriberchangerequest_options.py +25 -0
- wbmailing-2.2.1/wbmailing/migrations/__init__.py +0 -0
- wbmailing-2.2.1/wbmailing/models/__init__.py +6 -0
- wbmailing-2.2.1/wbmailing/models/mailing_lists.py +386 -0
- wbmailing-2.2.1/wbmailing/models/mails.py +895 -0
- wbmailing-2.2.1/wbmailing/serializers/__init__.py +19 -0
- wbmailing-2.2.1/wbmailing/serializers/mailing_lists.py +209 -0
- wbmailing-2.2.1/wbmailing/serializers/mails.py +251 -0
- wbmailing-2.2.1/wbmailing/tasks.py +37 -0
- wbmailing-2.2.1/wbmailing/templates/email_base_template.html +291 -0
- wbmailing-2.2.1/wbmailing/templates/mailing/maintain_mail_subsciptions.html +7 -0
- wbmailing-2.2.1/wbmailing/templates/mailing/manage_mailing_list_subscriptions.html +26 -0
- wbmailing-2.2.1/wbmailing/templates/template.html +295 -0
- wbmailing-2.2.1/wbmailing/templates/test.html +294 -0
- wbmailing-2.2.1/wbmailing/templates/workbench.html +24 -0
- wbmailing-2.2.1/wbmailing/templatetags/__init__.py +0 -0
- wbmailing-2.2.1/wbmailing/templatetags/mailing_tags.py +22 -0
- wbmailing-2.2.1/wbmailing/tests/__init__.py +0 -0
- wbmailing-2.2.1/wbmailing/tests/conftest.py +30 -0
- wbmailing-2.2.1/wbmailing/tests/models/__init__.py +0 -0
- wbmailing-2.2.1/wbmailing/tests/models/test_mailing_lists.py +297 -0
- wbmailing-2.2.1/wbmailing/tests/models/test_mails.py +205 -0
- wbmailing-2.2.1/wbmailing/tests/signals.py +124 -0
- wbmailing-2.2.1/wbmailing/tests/test_serializers.py +28 -0
- wbmailing-2.2.1/wbmailing/tests/test_tasks.py +49 -0
- wbmailing-2.2.1/wbmailing/tests/test_viewsets.py +216 -0
- wbmailing-2.2.1/wbmailing/tests/tests.py +142 -0
- wbmailing-2.2.1/wbmailing/urls.py +90 -0
- wbmailing-2.2.1/wbmailing/viewsets/__init__.py +32 -0
- wbmailing-2.2.1/wbmailing/viewsets/analytics.py +110 -0
- wbmailing-2.2.1/wbmailing/viewsets/buttons/__init__.py +10 -0
- wbmailing-2.2.1/wbmailing/viewsets/buttons/mailing_lists.py +91 -0
- wbmailing-2.2.1/wbmailing/viewsets/buttons/mails.py +98 -0
- wbmailing-2.2.1/wbmailing/viewsets/display/__init__.py +16 -0
- wbmailing-2.2.1/wbmailing/viewsets/display/mailing_lists.py +175 -0
- wbmailing-2.2.1/wbmailing/viewsets/display/mails.py +318 -0
- wbmailing-2.2.1/wbmailing/viewsets/endpoints/__init__.py +8 -0
- wbmailing-2.2.1/wbmailing/viewsets/endpoints/mailing_lists.py +86 -0
- wbmailing-2.2.1/wbmailing/viewsets/endpoints/mails.py +51 -0
- wbmailing-2.2.1/wbmailing/viewsets/mailing_lists.py +320 -0
- wbmailing-2.2.1/wbmailing/viewsets/mails.py +425 -0
- wbmailing-2.2.1/wbmailing/viewsets/menu/__init__.py +5 -0
- wbmailing-2.2.1/wbmailing/viewsets/menu/mailing_lists.py +37 -0
- wbmailing-2.2.1/wbmailing/viewsets/menu/mails.py +25 -0
- wbmailing-2.2.1/wbmailing/viewsets/titles/__init__.py +17 -0
- wbmailing-2.2.1/wbmailing/viewsets/titles/mailing_lists.py +63 -0
- wbmailing-2.2.1/wbmailing/viewsets/titles/mails.py +55 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
~
|
|
2
|
+
|
|
3
|
+
# Docker volumes
|
|
4
|
+
volumes/
|
|
5
|
+
# Poetry auth file
|
|
6
|
+
auth.toml
|
|
7
|
+
|
|
8
|
+
media/*
|
|
9
|
+
media/
|
|
10
|
+
mediafiles/
|
|
11
|
+
mediafiles/*
|
|
12
|
+
test/*
|
|
13
|
+
staticfiles/*
|
|
14
|
+
staticfiles/
|
|
15
|
+
#
|
|
16
|
+
# Byte-compiled / optimized / DLL files
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*$py.class
|
|
20
|
+
|
|
21
|
+
# C extensions
|
|
22
|
+
*.so
|
|
23
|
+
|
|
24
|
+
# Distribution / packaging
|
|
25
|
+
.Python
|
|
26
|
+
build/
|
|
27
|
+
develop-eggs/
|
|
28
|
+
dist/
|
|
29
|
+
info/
|
|
30
|
+
downloads/
|
|
31
|
+
eggs/
|
|
32
|
+
.eggs/
|
|
33
|
+
lib/
|
|
34
|
+
lib64/
|
|
35
|
+
parts/
|
|
36
|
+
sdist/
|
|
37
|
+
var/
|
|
38
|
+
wheels/
|
|
39
|
+
share/python-wheels/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
.installed.cfg
|
|
42
|
+
*.egg
|
|
43
|
+
MANIFEST
|
|
44
|
+
|
|
45
|
+
# PyInstaller
|
|
46
|
+
# Usually these files are written by a python script from a template
|
|
47
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
48
|
+
*.manifest
|
|
49
|
+
*.spec
|
|
50
|
+
|
|
51
|
+
# Installer logs
|
|
52
|
+
pip-log.txt
|
|
53
|
+
pip-delete-this-directory.txt
|
|
54
|
+
|
|
55
|
+
# Unit test / coverage reports
|
|
56
|
+
htmlcov/
|
|
57
|
+
.tox/
|
|
58
|
+
.nox/
|
|
59
|
+
.coverage
|
|
60
|
+
.coverage.*
|
|
61
|
+
.cache
|
|
62
|
+
.dccache
|
|
63
|
+
nosetests.xml
|
|
64
|
+
coverage.xml
|
|
65
|
+
*.cover
|
|
66
|
+
*.py,cover
|
|
67
|
+
.hypothesis/
|
|
68
|
+
.pytest_cache/
|
|
69
|
+
cover/
|
|
70
|
+
report.xml
|
|
71
|
+
*/report.xml
|
|
72
|
+
|
|
73
|
+
# Translations
|
|
74
|
+
*.mo
|
|
75
|
+
*.pot
|
|
76
|
+
|
|
77
|
+
# Django stuff:
|
|
78
|
+
*.log
|
|
79
|
+
local_settings.py
|
|
80
|
+
*.sqlite3
|
|
81
|
+
db.sqlite3-journal
|
|
82
|
+
|
|
83
|
+
# Flask stuff:
|
|
84
|
+
instance/
|
|
85
|
+
.webassets-cache
|
|
86
|
+
|
|
87
|
+
# Scrapy stuff:
|
|
88
|
+
.scrapy
|
|
89
|
+
|
|
90
|
+
# Sphinx documentation
|
|
91
|
+
docs/_build/
|
|
92
|
+
|
|
93
|
+
# PyBuilder
|
|
94
|
+
.pybuilder/
|
|
95
|
+
target/
|
|
96
|
+
|
|
97
|
+
# Jupyter Notebook
|
|
98
|
+
.ipynb_checkpoints
|
|
99
|
+
*.ipynb
|
|
100
|
+
# IPython
|
|
101
|
+
profile_default/
|
|
102
|
+
ipython_config.py
|
|
103
|
+
|
|
104
|
+
# pyenv
|
|
105
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
106
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
107
|
+
# .python-version
|
|
108
|
+
|
|
109
|
+
# pipenv
|
|
110
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
111
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
112
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
113
|
+
# install all needed dependencies.
|
|
114
|
+
#Pipfile.lock
|
|
115
|
+
|
|
116
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
117
|
+
__pypackages__/
|
|
118
|
+
|
|
119
|
+
# Celery stuff
|
|
120
|
+
celerybeat-schedule
|
|
121
|
+
celerybeat.pid
|
|
122
|
+
|
|
123
|
+
# SageMath parsed files
|
|
124
|
+
*.sage.py
|
|
125
|
+
|
|
126
|
+
# Environments
|
|
127
|
+
.env
|
|
128
|
+
.envrc
|
|
129
|
+
.venv
|
|
130
|
+
env/
|
|
131
|
+
venv/
|
|
132
|
+
ENV/
|
|
133
|
+
env.bak/
|
|
134
|
+
venv.bak/
|
|
135
|
+
.vscode/
|
|
136
|
+
.idea/
|
|
137
|
+
.idea.bkp/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
crm/
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# Gitlab Runner
|
|
164
|
+
builds
|
|
165
|
+
builds/
|
|
166
|
+
|
|
167
|
+
# Integrator Office 365 : reverse proxy tunnel for outlook365
|
|
168
|
+
ngrok
|
|
169
|
+
*/ngrok
|
|
170
|
+
/modules/**/system/
|
|
171
|
+
|
|
172
|
+
/modules/wbmailing/files/*
|
|
173
|
+
/modules/wbmailing/mailing/*
|
|
174
|
+
|
|
175
|
+
/projects/*/requirements.txt
|
|
176
|
+
public
|
|
177
|
+
|
|
178
|
+
# Ignore archive localization generated folder
|
|
179
|
+
backend/modules/**/archive/*
|
|
180
|
+
**/**/requirements.txt
|
|
181
|
+
CHANGELOG-*
|
wbmailing-2.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wbmailing"
|
|
3
|
+
description = ""
|
|
4
|
+
authors = [{ name = "Christopher Wittlinger", email = "c.wittlinger@stainly.com"}]
|
|
5
|
+
dynamic = ["version"]
|
|
6
|
+
|
|
7
|
+
dependencies = [
|
|
8
|
+
"wbcore",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[tool.uv.sources]
|
|
12
|
+
wbcore = { workspace = true }
|
|
13
|
+
|
|
14
|
+
[tool.uv]
|
|
15
|
+
package = true
|
|
16
|
+
|
|
17
|
+
[tool.hatch.version]
|
|
18
|
+
path = "../../pyproject.toml"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.sdist]
|
|
21
|
+
include = ["wbmailing/*"]
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["wbmailing"]
|
|
25
|
+
only-packages = true
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
from .models import (
|
|
5
|
+
Mail,
|
|
6
|
+
MailEvent,
|
|
7
|
+
MailingList,
|
|
8
|
+
MailingListEmailContactThroughModel,
|
|
9
|
+
MailingListSubscriberChangeRequest,
|
|
10
|
+
MailTemplate,
|
|
11
|
+
MassMail,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MailingListEmailContactThroughInlineAdmin(admin.TabularInline):
|
|
16
|
+
model = MailingListEmailContactThroughModel
|
|
17
|
+
fk_name = "mailing_list"
|
|
18
|
+
autocomplete_fields = ["email_contact"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@admin.register(MailingList)
|
|
22
|
+
class MailingListAdmin(admin.ModelAdmin):
|
|
23
|
+
autocomplete_fields = ["email_contacts"]
|
|
24
|
+
search_fields = ["title"]
|
|
25
|
+
inlines = [
|
|
26
|
+
MailingListEmailContactThroughInlineAdmin,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@admin.register(MassMail)
|
|
31
|
+
class MassMailAdmin(admin.ModelAdmin):
|
|
32
|
+
autocomplete_fields = ["creator"]
|
|
33
|
+
|
|
34
|
+
def send_test_mail(self, request, queryset):
|
|
35
|
+
for mass_mail in queryset:
|
|
36
|
+
mass_mail.send_test_mail(request.user)
|
|
37
|
+
|
|
38
|
+
actions = [send_test_mail]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@admin.register(MailingListSubscriberChangeRequest)
|
|
42
|
+
class MailingListSubscriberChangeRequestAdmin(admin.ModelAdmin):
|
|
43
|
+
list_display = ("email_contact", "mailing_list", "status")
|
|
44
|
+
autocomplete_fields = ["email_contact", "requester"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
admin.site.register(MailEvent)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MailEventInline(admin.TabularInline):
|
|
51
|
+
model = MailEvent
|
|
52
|
+
fields = ("timestamp", "event_type", "reject_reason", "recipient", "user_agent", "tags", "description")
|
|
53
|
+
readonly_fields = ("timestamp", "event_type", "reject_reason", "recipient", "user_agent", "tags", "description")
|
|
54
|
+
extra = 0
|
|
55
|
+
can_delete = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@admin.register(Mail)
|
|
59
|
+
class MailAdmin(admin.ModelAdmin):
|
|
60
|
+
def send_mails(self, request, queryset):
|
|
61
|
+
for mail in queryset:
|
|
62
|
+
mail.resend()
|
|
63
|
+
|
|
64
|
+
send_mails.short_description = _("Send Emails")
|
|
65
|
+
actions = [send_mails]
|
|
66
|
+
search_fields = ["mass_mail__subject", "from_email", "to_email__address", "subject", "message_ids"]
|
|
67
|
+
autocomplete_fields = ["to_email", "cc_email", "bcc_email"]
|
|
68
|
+
|
|
69
|
+
inlines = [MailEventInline]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@admin.register(MailTemplate)
|
|
73
|
+
class MailTemplateAdmin(admin.ModelAdmin):
|
|
74
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.db.models.signals import post_migrate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WbmailingConfig(AppConfig):
|
|
6
|
+
name = "wbmailing"
|
|
7
|
+
|
|
8
|
+
def ready(self) -> None:
|
|
9
|
+
from wbmailing.management import initialize_task
|
|
10
|
+
|
|
11
|
+
post_migrate.connect(
|
|
12
|
+
initialize_task,
|
|
13
|
+
dispatch_uid="wbmailing.initialize_task",
|
|
14
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from anymail.backends.mailgun import EmailBackend as AnymailMailgunBackend
|
|
4
|
+
from anymail.backends.mailjet import EmailBackend as AnymailMailjetBackend
|
|
5
|
+
from anymail.backends.mandrill import EmailBackend as AnymailMandrillBackend
|
|
6
|
+
from anymail.backends.postmark import EmailBackend as AnymailPostmarkBackend
|
|
7
|
+
from anymail.backends.sendgrid import EmailBackend as AnymailSendgridBackend
|
|
8
|
+
from anymail.backends.sendinblue import EmailBackend as AnymailSendinblueBackend
|
|
9
|
+
from anymail.exceptions import AnymailError
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.core.mail.backends.console import EmailBackend as ConsoleBackend
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
from sentry_sdk import capture_message
|
|
14
|
+
from wbcore.utils.html import convert_html2text
|
|
15
|
+
from wbmailing.models import Mail, MailEvent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SendMessagesMixin:
|
|
19
|
+
def _process_msg(self, message):
|
|
20
|
+
mail = message.mail if hasattr(message, "mail") else None
|
|
21
|
+
mass_mail = message.mass_mail if hasattr(message, "mass_mail") else None
|
|
22
|
+
if mail:
|
|
23
|
+
event_type = MailEvent.EventType.RESENT
|
|
24
|
+
else:
|
|
25
|
+
if not hasattr(message, "silent_mail") or (hasattr(message, "silent_mail") and not message.silent_mail):
|
|
26
|
+
mail = Mail.create_mail_from_mailmessage(message, user=getattr(message, "user", None))
|
|
27
|
+
event_type = MailEvent.EventType.CREATED
|
|
28
|
+
if mass_mail:
|
|
29
|
+
message.tags = [f"massmail-{mass_mail.id}"]
|
|
30
|
+
else:
|
|
31
|
+
message.tags = [f"mail-{mail.id}"]
|
|
32
|
+
if mail and mail.body:
|
|
33
|
+
# We reset the body text and html field with what might have been computed in create_mail_from_mailmessage
|
|
34
|
+
message.body = convert_html2text(mail.body)
|
|
35
|
+
message.alternatives = []
|
|
36
|
+
message.attach_alternative(mail.body, "text/html")
|
|
37
|
+
return event_type, mail
|
|
38
|
+
|
|
39
|
+
def send_messages(self, email_messages):
|
|
40
|
+
"""
|
|
41
|
+
Sends one or more EmailMessage objects and returns the number of email
|
|
42
|
+
messages sent.
|
|
43
|
+
"""
|
|
44
|
+
# This API is specified by Django's core BaseEmailBackend
|
|
45
|
+
# (so you can't change it to, e.g., return detailed status).
|
|
46
|
+
# Subclasses shouldn't need to override.
|
|
47
|
+
from wbmailing.models import MailEvent
|
|
48
|
+
|
|
49
|
+
num_sent = 0
|
|
50
|
+
if not email_messages:
|
|
51
|
+
return num_sent
|
|
52
|
+
|
|
53
|
+
created_session = self.open()
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
for message in email_messages:
|
|
57
|
+
try:
|
|
58
|
+
event_type, mail = self._process_msg(message)
|
|
59
|
+
sent = self._send(message)
|
|
60
|
+
if mail:
|
|
61
|
+
MailEvent.objects.create(
|
|
62
|
+
mail=mail, event_type=event_type, timestamp=timezone.now() - timedelta(seconds=1)
|
|
63
|
+
)
|
|
64
|
+
mail.message_ids.append(message.anymail_status.message_id)
|
|
65
|
+
mail.last_send = timezone.now()
|
|
66
|
+
mail.save()
|
|
67
|
+
except AnymailError as e:
|
|
68
|
+
capture_message(e)
|
|
69
|
+
if self.fail_silently:
|
|
70
|
+
sent = False
|
|
71
|
+
else:
|
|
72
|
+
raise
|
|
73
|
+
if sent:
|
|
74
|
+
num_sent += 1
|
|
75
|
+
finally:
|
|
76
|
+
if created_session:
|
|
77
|
+
self.close()
|
|
78
|
+
|
|
79
|
+
return num_sent
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class PostmarkEmailBackend(SendMessagesMixin, AnymailPostmarkBackend):
|
|
83
|
+
def send_messages(self, email_messages):
|
|
84
|
+
if settings.WBMAILING_POSTMARK_BROADCAST_STREAM_ID:
|
|
85
|
+
for msg in email_messages:
|
|
86
|
+
if hasattr(msg, "mass_mail") and msg.mass_mail:
|
|
87
|
+
msg.esp_extra = {"MessageStream": settings.WBMAILING_POSTMARK_BROADCAST_STREAM_ID}
|
|
88
|
+
|
|
89
|
+
return super().send_messages(email_messages)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SendgridEmailBackend(SendMessagesMixin, AnymailSendgridBackend):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MailgunEmailBackend(SendMessagesMixin, AnymailMailgunBackend):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MailjetEmailBackend(SendMessagesMixin, AnymailMailjetBackend):
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class MandrillEmailBackend(SendMessagesMixin, AnymailMandrillBackend):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SendinblueEmailBackend(SendMessagesMixin, AnymailSendinblueBackend):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ConsoleEmailBackend(SendMessagesMixin, ConsoleBackend):
|
|
113
|
+
def send_messages(self, email_messages):
|
|
114
|
+
"""Write all messages to the stream in a thread-safe way."""
|
|
115
|
+
if not email_messages:
|
|
116
|
+
return
|
|
117
|
+
msg_count = 0
|
|
118
|
+
with self._lock:
|
|
119
|
+
try:
|
|
120
|
+
stream_created = self.open()
|
|
121
|
+
for message in email_messages:
|
|
122
|
+
self._process_msg(message)
|
|
123
|
+
self.write_message(message)
|
|
124
|
+
self.stream.flush() # flush after each message
|
|
125
|
+
msg_count += 1
|
|
126
|
+
if stream_created:
|
|
127
|
+
self.close()
|
|
128
|
+
except Exception:
|
|
129
|
+
if not self.fail_silently:
|
|
130
|
+
raise
|
|
131
|
+
return msg_count
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from re import fullmatch
|
|
2
|
+
|
|
3
|
+
from django.forms import ValidationError
|
|
4
|
+
from django.utils.translation import gettext as _
|
|
5
|
+
from dynamic_preferences.preferences import Section
|
|
6
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
7
|
+
from dynamic_preferences.types import BooleanPreference, StringPreference
|
|
8
|
+
|
|
9
|
+
mailing_section = Section("wbmailing")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@global_preferences_registry.register
|
|
13
|
+
class DefaultSourceMailPreference(StringPreference):
|
|
14
|
+
section = mailing_section
|
|
15
|
+
name = "default_source_mail"
|
|
16
|
+
default = "info@stainly-bench.com"
|
|
17
|
+
|
|
18
|
+
verbose_name = _("Default Source Mail Preference")
|
|
19
|
+
help_text = _("The default address used to send emails from")
|
|
20
|
+
|
|
21
|
+
def validate(self, value):
|
|
22
|
+
if not fullmatch(r"[^@]+@[^@]+\.[^@]+", value):
|
|
23
|
+
raise ValidationError(_("Not a valid email format"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@global_preferences_registry.register
|
|
27
|
+
class AutomaticallyApproveUnsubscriptionRequestFromHardBound(BooleanPreference):
|
|
28
|
+
section = mailing_section
|
|
29
|
+
name = "automatically_approve_unsubscription_request_from_hard_bounce"
|
|
30
|
+
default = False
|
|
31
|
+
|
|
32
|
+
verbose_name = _("Automatically approve unsubscription request from hard bounce")
|
|
33
|
+
help_text = _(
|
|
34
|
+
"Automatically approve unsubscription request from hard bounce received from the ESP tracking system"
|
|
35
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
import pytz
|
|
3
|
+
from wbcore.contrib.directory.factories import EmailContactFactory
|
|
4
|
+
from wbmailing.models import (
|
|
5
|
+
Mail,
|
|
6
|
+
MailEvent,
|
|
7
|
+
MailingList,
|
|
8
|
+
MailingListEmailContactThroughModel,
|
|
9
|
+
MailingListSubscriberChangeRequest,
|
|
10
|
+
MailTemplate,
|
|
11
|
+
MassMail,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MailingListSubscriberChangeRequestFactory(factory.django.DjangoModelFactory):
|
|
16
|
+
class Meta:
|
|
17
|
+
model = MailingListSubscriberChangeRequest
|
|
18
|
+
|
|
19
|
+
expiration_date = factory.Faker("date_object")
|
|
20
|
+
status = MailingListSubscriberChangeRequest.Status.PENDING
|
|
21
|
+
type = MailingListSubscriberChangeRequest.Type.SUBSCRIBING
|
|
22
|
+
email_contact = factory.SubFactory("wbcore.contrib.directory.factories.EmailContactFactory")
|
|
23
|
+
mailing_list = factory.SubFactory("wbmailing.factories.MailingListFactory")
|
|
24
|
+
requester = factory.SubFactory("wbcore.contrib.authentication.factories.AuthenticatedPersonFactory")
|
|
25
|
+
approver = factory.SubFactory("wbcore.contrib.authentication.factories.AuthenticatedPersonFactory")
|
|
26
|
+
reason = factory.Faker("text", max_nb_chars=256)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApprovedMailingListSubscriberChangeRequest(MailingListSubscriberChangeRequestFactory):
|
|
30
|
+
status = MailingListSubscriberChangeRequest.Status.APPROVED
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MailingListFactory(factory.django.DjangoModelFactory):
|
|
34
|
+
class Meta:
|
|
35
|
+
model = MailingList
|
|
36
|
+
|
|
37
|
+
title = factory.Faker("text", max_nb_chars=64)
|
|
38
|
+
is_public = False
|
|
39
|
+
|
|
40
|
+
@factory.post_generation
|
|
41
|
+
def email_contacts(self, create, extracted, **kwargs):
|
|
42
|
+
if not create:
|
|
43
|
+
return
|
|
44
|
+
if extracted:
|
|
45
|
+
for email_contact in extracted:
|
|
46
|
+
MailingListEmailContactThroughModel.objects.create(
|
|
47
|
+
mailing_list=self,
|
|
48
|
+
email_contact=email_contact,
|
|
49
|
+
status=MailingListEmailContactThroughModel.Status.SUBSCRIBED,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EmailContactMailingListFactory(MailingListFactory):
|
|
54
|
+
@factory.post_generation
|
|
55
|
+
def email_contacts(self, create, extracted, **kwargs):
|
|
56
|
+
mlscr = MailingListSubscriberChangeRequestFactory()
|
|
57
|
+
self.email_contacts.add(mlscr.email_contact)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MailingListEmailContactFactory(EmailContactFactory):
|
|
61
|
+
@factory.post_generation
|
|
62
|
+
def subscriptions(self, create, extracted, **kwargs):
|
|
63
|
+
ml = MailingListFactory()
|
|
64
|
+
MailingListEmailContactThroughModel.objects.create(
|
|
65
|
+
mailing_list=ml, email_contact=self, status=MailingListEmailContactThroughModel.Status.SUBSCRIBED
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class UnsubscribedMailingListEmailContactFactory(EmailContactFactory):
|
|
70
|
+
@factory.post_generation
|
|
71
|
+
def subscriptions(self, create, extracted, **kwargs):
|
|
72
|
+
ml = MailingListFactory()
|
|
73
|
+
MailingListEmailContactThroughModel.objects.create(
|
|
74
|
+
mailing_list=ml, email_contact=self, status=MailingListEmailContactThroughModel.Status.UNSUBSCRIBED
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MailingListEmailContactThroughModelFactory(factory.django.DjangoModelFactory):
|
|
79
|
+
email_contact = factory.SubFactory("wbcore.contrib.directory.factories.EmailContactFactory")
|
|
80
|
+
mailing_list = factory.SubFactory("wbmailing.factories.MailingListFactory")
|
|
81
|
+
status = MailingListEmailContactThroughModel.Status.SUBSCRIBED
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
model = MailingListEmailContactThroughModel
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MassMailFactory(factory.django.DjangoModelFactory):
|
|
88
|
+
class Meta:
|
|
89
|
+
model = MassMail
|
|
90
|
+
|
|
91
|
+
# status = #defaut = DRAFT
|
|
92
|
+
from_email = factory.Faker("email")
|
|
93
|
+
template = factory.SubFactory("wbmailing.factories.MailTemplateFactory")
|
|
94
|
+
|
|
95
|
+
@factory.post_generation
|
|
96
|
+
def mailing_lists(self, create, extracted, **kwargs):
|
|
97
|
+
if not create:
|
|
98
|
+
return
|
|
99
|
+
if extracted:
|
|
100
|
+
for mailing_list in extracted:
|
|
101
|
+
self.mailing_lists.add(mailing_list)
|
|
102
|
+
|
|
103
|
+
subject = factory.Faker("text", max_nb_chars=64)
|
|
104
|
+
body = factory.Faker("paragraph", nb_sentences=5)
|
|
105
|
+
|
|
106
|
+
@factory.post_generation
|
|
107
|
+
def attachments(self, create, extracted, **kwargs):
|
|
108
|
+
if not create:
|
|
109
|
+
return
|
|
110
|
+
if extracted:
|
|
111
|
+
for attachment in extracted:
|
|
112
|
+
self.attach_document(attachment)
|
|
113
|
+
|
|
114
|
+
# body_json = #JSONField(null=True, blank=True)
|
|
115
|
+
created = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
116
|
+
creator = factory.SubFactory("wbcore.contrib.directory.factories.PersonFactory")
|
|
117
|
+
send_at = factory.Faker("date_time_between", start_date="now", end_date="+30y", tzinfo=pytz.utc)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CustomMassMailFactory(MassMailFactory):
|
|
121
|
+
@factory.post_generation
|
|
122
|
+
def mailing_lists(self, create, extracted, **kwargs):
|
|
123
|
+
ml = MailingListFactory()
|
|
124
|
+
self.mailing_lists.add(ml)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class CustomMassMailEmailContactFactory(EmailContactFactory):
|
|
128
|
+
@factory.post_generation
|
|
129
|
+
def subscriptions(self, create, extracted, **kwargs):
|
|
130
|
+
ml = MailingListFactory.create()
|
|
131
|
+
ml.add_to_mailinglist(self)
|
|
132
|
+
MassMailFactory.create(mailing_lists=[ml])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class MailFactory(factory.django.DjangoModelFactory):
|
|
136
|
+
class Meta:
|
|
137
|
+
model = Mail
|
|
138
|
+
|
|
139
|
+
created = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
140
|
+
last_send = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
141
|
+
template = factory.SubFactory("wbmailing.factories.MailTemplateFactory")
|
|
142
|
+
message_ids = factory.List([factory.Faker("pystr")])
|
|
143
|
+
mass_mail = factory.SubFactory("wbmailing.factories.MassMailFactory")
|
|
144
|
+
from_email = factory.Faker("email")
|
|
145
|
+
|
|
146
|
+
@factory.post_generation
|
|
147
|
+
def to_email(self, create, extracted, **kwargs):
|
|
148
|
+
if not create:
|
|
149
|
+
return
|
|
150
|
+
if extracted:
|
|
151
|
+
for email in extracted:
|
|
152
|
+
self.to_email.add(email)
|
|
153
|
+
|
|
154
|
+
@factory.post_generation
|
|
155
|
+
def cc_email(self, create, extracted, **kwargs):
|
|
156
|
+
if not create:
|
|
157
|
+
return
|
|
158
|
+
if extracted:
|
|
159
|
+
for email in extracted:
|
|
160
|
+
self.cc_email.add(email)
|
|
161
|
+
|
|
162
|
+
@factory.post_generation
|
|
163
|
+
def bcc_email(self, create, extracted, **kwargs):
|
|
164
|
+
if not create:
|
|
165
|
+
return
|
|
166
|
+
if extracted:
|
|
167
|
+
for email in extracted:
|
|
168
|
+
self.bcc_email.add(email)
|
|
169
|
+
|
|
170
|
+
subject = factory.Faker("text", max_nb_chars=64)
|
|
171
|
+
body = factory.Faker("paragraph", nb_sentences=5)
|
|
172
|
+
# body_json = # JSONField(null=True, blank=True)
|
|
173
|
+
|
|
174
|
+
@factory.post_generation
|
|
175
|
+
def attachments(self, create, extracted, **kwargs):
|
|
176
|
+
if not create:
|
|
177
|
+
return
|
|
178
|
+
if extracted:
|
|
179
|
+
for attachment in extracted:
|
|
180
|
+
self.attach_document(attachment)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ToEmailMailFactory(MailFactory):
|
|
184
|
+
@factory.post_generation
|
|
185
|
+
def to_email(self, create, extracted, **kwargs):
|
|
186
|
+
ec = EmailContactFactory.create()
|
|
187
|
+
self.to_email.add(ec)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MailEventFactory(factory.django.DjangoModelFactory):
|
|
191
|
+
class Meta:
|
|
192
|
+
model = MailEvent
|
|
193
|
+
|
|
194
|
+
mail = factory.SubFactory("wbmailing.factories.MailFactory")
|
|
195
|
+
timestamp = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
196
|
+
# event_type = # default=EventType.CREATED
|
|
197
|
+
reject_reason = "" # default=null # RejectReason.choices
|
|
198
|
+
description = factory.Faker("paragraph", nb_sentences=2)
|
|
199
|
+
recipient = factory.Faker("email")
|
|
200
|
+
click_url = factory.Faker("image_url")
|
|
201
|
+
ip = factory.Faker("ipv4")
|
|
202
|
+
user_agent = factory.Faker("first_name")
|
|
203
|
+
# raw_data = JSONField(default=dict, null=True, blank=True, verbose_name="Raw Data")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class MailTemplateFactory(factory.django.DjangoModelFactory):
|
|
207
|
+
class Meta:
|
|
208
|
+
model = MailTemplate
|
|
209
|
+
|
|
210
|
+
title = factory.Faker("text", max_nb_chars=64)
|
|
211
|
+
template = factory.Faker("paragraph", nb_sentences=5)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .mailing_lists import (
|
|
2
|
+
EmailContactMailingListFilterSet,
|
|
3
|
+
MailingListEmailContactThroughModelModelFilterSet,
|
|
4
|
+
MailingListFilterSet,
|
|
5
|
+
MailingListSubscriberChangeRequestFilterSet,
|
|
6
|
+
MailStatusMassMailFilterSet,
|
|
7
|
+
)
|
|
8
|
+
from .mails import MailFilter, MassMailFilterSet
|