django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,390 @@
|
|
1
|
+
"""
|
2
|
+
Simple Python service API for sending emails via Mailbox + AWS SES with Django-templated support.
|
3
|
+
|
4
|
+
This module provides a minimal, ergonomic interface for Django/Python code
|
5
|
+
to send emails using a configured Mailbox and AWS SES. It reuses existing helpers
|
6
|
+
and persists a SentMessage record for observability and webhook status updates.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
|
10
|
+
from mojo.apps.aws.services import email as email_service
|
11
|
+
|
12
|
+
# Simple send (no template)
|
13
|
+
sent = email_service.send_email(
|
14
|
+
from_email="support@example.com",
|
15
|
+
to=["user1@example.org", "user2@example.org"],
|
16
|
+
subject="Welcome",
|
17
|
+
body_text="Welcome to our service!",
|
18
|
+
body_html="<p>Welcome to our service!</p>",
|
19
|
+
)
|
20
|
+
print(sent.id, sent.ses_message_id, sent.status)
|
21
|
+
|
22
|
+
# Send with Django EmailTemplate (stored in DB)
|
23
|
+
sent = email_service.send_with_template(
|
24
|
+
from_email="support@example.com",
|
25
|
+
to="user@example.org",
|
26
|
+
template_name="welcome",
|
27
|
+
context={"first_name": "Ada"}
|
28
|
+
)
|
29
|
+
|
30
|
+
# Send with SES template (must exist in SES)
|
31
|
+
sent = email_service.send_template_email(
|
32
|
+
from_email="support@example.com",
|
33
|
+
to="user@example.org",
|
34
|
+
template_name="welcome-template",
|
35
|
+
template_context={"first_name": "Ada"}
|
36
|
+
)
|
37
|
+
|
38
|
+
Notes:
|
39
|
+
- Only Mailbox-owned addresses can send (enforced via allow_outbound flag).
|
40
|
+
- The envelope MAIL FROM and From header will be the Mailbox email.
|
41
|
+
- Domain verification is required unless allow_unverified=True is passed.
|
42
|
+
- Attachments are not supported by this simple API. To send attachments,
|
43
|
+
you can build a MIME message and call EmailSender.send_raw_email directly.
|
44
|
+
- reply_to defaults to from_email, and can be overridden.
|
45
|
+
|
46
|
+
This API stores a SentMessage row immediately with status="sending" and SES MessageId
|
47
|
+
(if available). Final delivery/bounce/complaint updates are handled asynchronously
|
48
|
+
by the SNS webhooks.
|
49
|
+
"""
|
50
|
+
|
51
|
+
from __future__ import annotations
|
52
|
+
|
53
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
54
|
+
from django.db import transaction
|
55
|
+
|
56
|
+
from mojo.apps.aws.models import Mailbox, SentMessage, EmailDomain, EmailTemplate
|
57
|
+
from mojo.helpers.aws.ses import EmailSender
|
58
|
+
from mojo.helpers.settings import settings
|
59
|
+
from mojo.helpers import logit
|
60
|
+
|
61
|
+
|
62
|
+
logger = logit.get_logger("email", "email.log")
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
# Exceptions
|
67
|
+
|
68
|
+
class MailboxNotFound(Exception):
|
69
|
+
pass
|
70
|
+
|
71
|
+
|
72
|
+
class OutboundNotAllowed(Exception):
|
73
|
+
pass
|
74
|
+
|
75
|
+
|
76
|
+
class DomainNotVerified(Exception):
|
77
|
+
pass
|
78
|
+
|
79
|
+
|
80
|
+
# Internal helpers
|
81
|
+
|
82
|
+
def _as_list(value: Union[str, Sequence[str], None]) -> List[str]:
|
83
|
+
if value is None:
|
84
|
+
return []
|
85
|
+
if isinstance(value, (list, tuple)):
|
86
|
+
return [str(v).strip() for v in value if str(v).strip()]
|
87
|
+
v = str(value).strip()
|
88
|
+
return [v] if v else []
|
89
|
+
|
90
|
+
|
91
|
+
def _get_mailbox(from_email: str) -> Mailbox:
|
92
|
+
mb = Mailbox.objects.select_related("domain").filter(email__iexact=from_email.strip()).first()
|
93
|
+
if not mb:
|
94
|
+
raise MailboxNotFound(f"Mailbox not found for from_email={from_email!r}")
|
95
|
+
if not mb.allow_outbound:
|
96
|
+
raise OutboundNotAllowed(f"Outbound sending is disabled for mailbox {mb.email}")
|
97
|
+
return mb
|
98
|
+
|
99
|
+
|
100
|
+
def _choose_region(mb: Mailbox, region: Optional[str]) -> str:
|
101
|
+
return region or (mb.domain.region if isinstance(mb.domain, EmailDomain) else None) or getattr(settings, "AWS_REGION", "us-east-1")
|
102
|
+
|
103
|
+
|
104
|
+
def _check_domain_verified(mb: Mailbox, allow_unverified: bool):
|
105
|
+
if allow_unverified:
|
106
|
+
return
|
107
|
+
if not mb.domain.is_verified:
|
108
|
+
raise DomainNotVerified(f"Domain {mb.domain.name if mb.domain_id else '(unknown)'} is not verified for sending (status={mb.domain.status})")
|
109
|
+
|
110
|
+
|
111
|
+
def _get_sender(access_key: Optional[str], secret_key: Optional[str], region: str) -> EmailSender:
|
112
|
+
return EmailSender(
|
113
|
+
access_key=access_key or settings.AWS_KEY,
|
114
|
+
secret_key=secret_key or settings.AWS_SECRET,
|
115
|
+
region=region,
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
# Public API
|
120
|
+
|
121
|
+
def send_email(
|
122
|
+
from_email: str,
|
123
|
+
to: Union[str, Sequence[str]],
|
124
|
+
*,
|
125
|
+
subject: Optional[str] = None,
|
126
|
+
body_text: Optional[str] = None,
|
127
|
+
body_html: Optional[str] = None,
|
128
|
+
cc: Optional[Union[str, Sequence[str]]] = None,
|
129
|
+
bcc: Optional[Union[str, Sequence[str]]] = None,
|
130
|
+
reply_to: Optional[Union[str, Sequence[str]]] = None,
|
131
|
+
allow_unverified: bool = False,
|
132
|
+
aws_access_key: Optional[str] = None,
|
133
|
+
aws_secret_key: Optional[str] = None,
|
134
|
+
region: Optional[str] = None,
|
135
|
+
) -> SentMessage:
|
136
|
+
"""
|
137
|
+
Send an email using a Mailbox (resolved by from_email) and AWS SES.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
from_email: The sending address (must match a configured Mailbox).
|
141
|
+
to: One or more recipient addresses.
|
142
|
+
subject: Email subject (required if no template is used).
|
143
|
+
body_text: Optional plain text body.
|
144
|
+
body_html: Optional HTML body.
|
145
|
+
cc, bcc, reply_to: Optional addressing.
|
146
|
+
allow_unverified: If True, bypass domain verification check (use with caution).
|
147
|
+
aws_access_key, aws_secret_key: Optional per-call AWS credentials.
|
148
|
+
region: Optional AWS region; defaults to mailbox.domain.region or settings.AWS_REGION.
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
SentMessage instance persisted to the database.
|
152
|
+
|
153
|
+
Raises:
|
154
|
+
MailboxNotFound, OutboundNotAllowed, DomainNotVerified on validation errors.
|
155
|
+
"""
|
156
|
+
mailbox = _get_mailbox(from_email)
|
157
|
+
_check_domain_verified(mailbox, allow_unverified)
|
158
|
+
|
159
|
+
region_final = _choose_region(mailbox, region)
|
160
|
+
to_list = _as_list(to)
|
161
|
+
cc_list = _as_list(cc)
|
162
|
+
bcc_list = _as_list(bcc)
|
163
|
+
# Default reply_to to from_email if not provided
|
164
|
+
reply_to_list = _as_list(reply_to) or [mailbox.email]
|
165
|
+
|
166
|
+
if not to_list:
|
167
|
+
raise ValueError("At least one 'to' recipient is required")
|
168
|
+
if not (subject or body_text or body_html):
|
169
|
+
raise ValueError("Provide at least one of subject, body_text, or body_html")
|
170
|
+
|
171
|
+
sender = _get_sender(aws_access_key, aws_secret_key, region_final)
|
172
|
+
|
173
|
+
with transaction.atomic():
|
174
|
+
sent = SentMessage.objects.create(
|
175
|
+
mailbox=mailbox,
|
176
|
+
to_addresses=to_list,
|
177
|
+
cc_addresses=cc_list,
|
178
|
+
bcc_addresses=bcc_list,
|
179
|
+
subject=subject or None,
|
180
|
+
body_text=body_text,
|
181
|
+
body_html=body_html,
|
182
|
+
status=SentMessage.STATUS_SENDING,
|
183
|
+
)
|
184
|
+
|
185
|
+
try:
|
186
|
+
resp = sender.send_email(
|
187
|
+
source=mailbox.email,
|
188
|
+
to_addresses=to_list,
|
189
|
+
subject=subject or "",
|
190
|
+
body_text=body_text,
|
191
|
+
body_html=body_html,
|
192
|
+
cc_addresses=cc_list or None,
|
193
|
+
bcc_addresses=bcc_list or None,
|
194
|
+
reply_to_addresses=reply_to_list or None,
|
195
|
+
)
|
196
|
+
msg_id = resp.get("MessageId")
|
197
|
+
if msg_id:
|
198
|
+
sent.ses_message_id = msg_id
|
199
|
+
sent.save(update_fields=["ses_message_id", "modified"])
|
200
|
+
return sent
|
201
|
+
# Failure path
|
202
|
+
sent.status = SentMessage.STATUS_FAILED
|
203
|
+
sent.status_reason = resp.get("Error") or str(resp)
|
204
|
+
sent.save(update_fields=["status", "status_reason", "modified"])
|
205
|
+
return sent
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
logger.error(f"send_email error for mailbox={mailbox.email}: {e}")
|
209
|
+
sent.status = SentMessage.STATUS_FAILED
|
210
|
+
sent.status_reason = str(e)
|
211
|
+
sent.save(update_fields=["status", "status_reason", "modified"])
|
212
|
+
return sent
|
213
|
+
|
214
|
+
|
215
|
+
def send_with_template(
|
216
|
+
from_email: str,
|
217
|
+
to: Union[str, Sequence[str]],
|
218
|
+
*,
|
219
|
+
template_name: str,
|
220
|
+
context: Optional[Dict[str, Any]] = None,
|
221
|
+
cc: Optional[Union[str, Sequence[str]]] = None,
|
222
|
+
bcc: Optional[Union[str, Sequence[str]]] = None,
|
223
|
+
reply_to: Optional[Union[str, Sequence[str]]] = None,
|
224
|
+
allow_unverified: bool = False,
|
225
|
+
aws_access_key: Optional[str] = None,
|
226
|
+
aws_secret_key: Optional[str] = None,
|
227
|
+
region: Optional[str] = None,
|
228
|
+
) -> SentMessage:
|
229
|
+
"""
|
230
|
+
Send using a Django EmailTemplate stored in DB.
|
231
|
+
Renders subject/text/html with the provided context and sends via SES.
|
232
|
+
"""
|
233
|
+
mailbox = _get_mailbox(from_email)
|
234
|
+
_check_domain_verified(mailbox, allow_unverified)
|
235
|
+
|
236
|
+
region_final = _choose_region(mailbox, region)
|
237
|
+
to_list = _as_list(to)
|
238
|
+
cc_list = _as_list(cc)
|
239
|
+
bcc_list = _as_list(bcc)
|
240
|
+
# Default reply_to to from_email if not provided
|
241
|
+
reply_to_list = _as_list(reply_to) or [mailbox.email]
|
242
|
+
|
243
|
+
if not to_list:
|
244
|
+
raise ValueError("At least one 'to' recipient is required")
|
245
|
+
if not template_name:
|
246
|
+
raise ValueError("template_name is required")
|
247
|
+
|
248
|
+
# Load and render template
|
249
|
+
tpl = EmailTemplate.objects.filter(name=template_name).first()
|
250
|
+
if not tpl:
|
251
|
+
raise ValueError(f"EmailTemplate not found: {template_name}")
|
252
|
+
rendered = tpl.render_all(context or {})
|
253
|
+
|
254
|
+
subject = (rendered.get("subject") or "").strip()
|
255
|
+
body_text = rendered.get("text")
|
256
|
+
body_html = rendered.get("html")
|
257
|
+
|
258
|
+
if not (subject or body_text or body_html):
|
259
|
+
raise ValueError("Rendered template produced no subject/text/html")
|
260
|
+
|
261
|
+
sender = _get_sender(aws_access_key, aws_secret_key, region_final)
|
262
|
+
|
263
|
+
with transaction.atomic():
|
264
|
+
sent = SentMessage.objects.create(
|
265
|
+
mailbox=mailbox,
|
266
|
+
to_addresses=to_list,
|
267
|
+
cc_addresses=cc_list,
|
268
|
+
bcc_addresses=bcc_list,
|
269
|
+
subject=subject or None,
|
270
|
+
body_text=body_text,
|
271
|
+
body_html=body_html,
|
272
|
+
template_name=tpl.name,
|
273
|
+
template_context=context or {},
|
274
|
+
status=SentMessage.STATUS_SENDING,
|
275
|
+
)
|
276
|
+
|
277
|
+
try:
|
278
|
+
resp = sender.send_email(
|
279
|
+
source=mailbox.email,
|
280
|
+
to_addresses=to_list,
|
281
|
+
subject=subject or "",
|
282
|
+
body_text=body_text,
|
283
|
+
body_html=body_html,
|
284
|
+
cc_addresses=cc_list or None,
|
285
|
+
bcc_addresses=bcc_list or None,
|
286
|
+
reply_to_addresses=reply_to_list or None,
|
287
|
+
)
|
288
|
+
msg_id = resp.get("MessageId")
|
289
|
+
if msg_id:
|
290
|
+
sent.ses_message_id = msg_id
|
291
|
+
sent.save(update_fields=["ses_message_id", "modified"])
|
292
|
+
return sent
|
293
|
+
# Failure path
|
294
|
+
sent.status = SentMessage.STATUS_FAILED
|
295
|
+
sent.status_reason = resp.get("Error") or str(resp)
|
296
|
+
sent.save(update_fields=["status", "status_reason", "modified"])
|
297
|
+
return sent
|
298
|
+
except Exception as e:
|
299
|
+
logger.error(f"send_with_template error for mailbox={mailbox.email}: {e}")
|
300
|
+
sent.status = SentMessage.STATUS_FAILED
|
301
|
+
sent.status_reason = str(e)
|
302
|
+
sent.save(update_fields=["status", "status_reason", "modified"])
|
303
|
+
return sent
|
304
|
+
|
305
|
+
def send_template_email(
|
306
|
+
from_email: str,
|
307
|
+
to: Union[str, Sequence[str]],
|
308
|
+
*,
|
309
|
+
template_name: str,
|
310
|
+
template_context: Optional[Dict[str, Any]] = None,
|
311
|
+
cc: Optional[Union[str, Sequence[str]]] = None,
|
312
|
+
bcc: Optional[Union[str, Sequence[str]]] = None,
|
313
|
+
reply_to: Optional[Union[str, Sequence[str]]] = None,
|
314
|
+
allow_unverified: bool = False,
|
315
|
+
aws_access_key: Optional[str] = None,
|
316
|
+
aws_secret_key: Optional[str] = None,
|
317
|
+
region: Optional[str] = None,
|
318
|
+
) -> SentMessage:
|
319
|
+
"""
|
320
|
+
Send an email using a SES template and a Mailbox (resolved by from_email).
|
321
|
+
|
322
|
+
Args:
|
323
|
+
from_email: The sending address (must match a configured Mailbox).
|
324
|
+
to: One or more recipient addresses.
|
325
|
+
template_name: Name of the SES template.
|
326
|
+
template_context: Dict used to render the SES template.
|
327
|
+
cc, bcc, reply_to: Optional addressing.
|
328
|
+
allow_unverified: If True, bypass domain verification check (use with caution).
|
329
|
+
aws_access_key, aws_secret_key: Optional per-call AWS credentials.
|
330
|
+
region: Optional AWS region; defaults to mailbox.domain.region or settings.AWS_REGION.
|
331
|
+
|
332
|
+
Returns:
|
333
|
+
SentMessage instance persisted to the database.
|
334
|
+
"""
|
335
|
+
mailbox = _get_mailbox(from_email)
|
336
|
+
_check_domain_verified(mailbox, allow_unverified)
|
337
|
+
|
338
|
+
region_final = _choose_region(mailbox, region)
|
339
|
+
to_list = _as_list(to)
|
340
|
+
cc_list = _as_list(cc)
|
341
|
+
bcc_list = _as_list(bcc)
|
342
|
+
# Default reply_to to from_email if not provided
|
343
|
+
reply_to_list = _as_list(reply_to) or [mailbox.email]
|
344
|
+
template_context = template_context if isinstance(template_context, dict) else {}
|
345
|
+
|
346
|
+
if not to_list:
|
347
|
+
raise ValueError("At least one 'to' recipient is required")
|
348
|
+
if not template_name:
|
349
|
+
raise ValueError("template_name is required for template-based sending")
|
350
|
+
|
351
|
+
sender = _get_sender(aws_access_key, aws_secret_key, region_final)
|
352
|
+
|
353
|
+
with transaction.atomic():
|
354
|
+
sent = SentMessage.objects.create(
|
355
|
+
mailbox=mailbox,
|
356
|
+
to_addresses=to_list,
|
357
|
+
cc_addresses=cc_list,
|
358
|
+
bcc_addresses=bcc_list,
|
359
|
+
template_name=template_name,
|
360
|
+
template_context=template_context,
|
361
|
+
status=SentMessage.STATUS_SENDING,
|
362
|
+
)
|
363
|
+
|
364
|
+
try:
|
365
|
+
resp = sender.send_template_email(
|
366
|
+
source=mailbox.email,
|
367
|
+
to_addresses=to_list,
|
368
|
+
template_name=template_name,
|
369
|
+
template_data=template_context,
|
370
|
+
cc_addresses=cc_list or None,
|
371
|
+
bcc_addresses=bcc_list or None,
|
372
|
+
reply_to_addresses=reply_to_list or None,
|
373
|
+
)
|
374
|
+
msg_id = resp.get("MessageId")
|
375
|
+
if msg_id:
|
376
|
+
sent.ses_message_id = msg_id
|
377
|
+
sent.save(update_fields=["ses_message_id", "modified"])
|
378
|
+
return sent
|
379
|
+
# Failure path
|
380
|
+
sent.status = SentMessage.STATUS_FAILED
|
381
|
+
sent.status_reason = resp.get("Error") or str(resp)
|
382
|
+
sent.save(update_fields=["status", "status_reason", "modified"])
|
383
|
+
return sent
|
384
|
+
|
385
|
+
except Exception as e:
|
386
|
+
logger.error(f"send_template_email error for mailbox={mailbox.email}: {e}")
|
387
|
+
sent.status = SentMessage.STATUS_FAILED
|
388
|
+
sent.status_reason = str(e)
|
389
|
+
sent.save(update_fields=["status", "status_reason", "modified"])
|
390
|
+
return sent
|