django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- 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 +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- 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 +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- 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 +6 -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/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/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- 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 +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- 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/models/log.py +3 -0
- 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 +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -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 +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- 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/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- 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/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 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class SentMessage(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
SentMessage
|
8
|
+
|
9
|
+
Represents an outbound email sent via AWS SES using a specific Mailbox (email address).
|
10
|
+
Tracks SES MessageId and delivery lifecycle (delivery, bounce, complaint) updated via SNS webhooks.
|
11
|
+
|
12
|
+
Notes:
|
13
|
+
- `mailbox` identifies the sending address; sending is only allowed when mailbox.allow_outbound is True.
|
14
|
+
- `ses_message_id` is populated after a successful SES send API call.
|
15
|
+
- `to_addresses`, `cc_addresses`, `bcc_addresses` are stored as JSON arrays.
|
16
|
+
- `template_name` and `template_context` support simple templated sending (EmailTemplate model can be added later).
|
17
|
+
- `status` reflects the current delivery state; `status_reason` stores detailed info (bounce/complaint payloads, errors).
|
18
|
+
"""
|
19
|
+
|
20
|
+
STATUS_QUEUED = "queued"
|
21
|
+
STATUS_SENDING = "sending"
|
22
|
+
STATUS_DELIVERED = "delivered"
|
23
|
+
STATUS_BOUNCED = "bounced"
|
24
|
+
STATUS_COMPLAINED = "complained"
|
25
|
+
STATUS_FAILED = "failed"
|
26
|
+
STATUS_UNKNOWN = "unknown"
|
27
|
+
|
28
|
+
STATUS_CHOICES = [
|
29
|
+
(STATUS_QUEUED, "Queued"),
|
30
|
+
(STATUS_SENDING, "Sending"),
|
31
|
+
(STATUS_DELIVERED, "Delivered"),
|
32
|
+
(STATUS_BOUNCED, "Bounced"),
|
33
|
+
(STATUS_COMPLAINED, "Complained"),
|
34
|
+
(STATUS_FAILED, "Failed"),
|
35
|
+
(STATUS_UNKNOWN, "Unknown"),
|
36
|
+
]
|
37
|
+
|
38
|
+
created = models.DateTimeField(auto_now_add=True, editable=False)
|
39
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
40
|
+
|
41
|
+
mailbox = models.ForeignKey(
|
42
|
+
"aws.Mailbox",
|
43
|
+
related_name="sent_messages",
|
44
|
+
on_delete=models.CASCADE,
|
45
|
+
help_text="Mailbox used as the sender (envelope MAIL FROM = mailbox.email)"
|
46
|
+
)
|
47
|
+
|
48
|
+
ses_message_id = models.CharField(
|
49
|
+
max_length=255,
|
50
|
+
null=True,
|
51
|
+
blank=True,
|
52
|
+
db_index=True,
|
53
|
+
help_text="AWS SES MessageId returned after a successful send"
|
54
|
+
)
|
55
|
+
|
56
|
+
# Recipients
|
57
|
+
to_addresses = models.JSONField(
|
58
|
+
default=list,
|
59
|
+
blank=True,
|
60
|
+
help_text="List of recipient addresses (To)"
|
61
|
+
)
|
62
|
+
cc_addresses = models.JSONField(
|
63
|
+
default=list,
|
64
|
+
blank=True,
|
65
|
+
help_text="List of recipient addresses (Cc)"
|
66
|
+
)
|
67
|
+
bcc_addresses = models.JSONField(
|
68
|
+
default=list,
|
69
|
+
blank=True,
|
70
|
+
help_text="List of recipient addresses (Bcc)"
|
71
|
+
)
|
72
|
+
|
73
|
+
# Content
|
74
|
+
subject = models.CharField(
|
75
|
+
max_length=512,
|
76
|
+
null=True,
|
77
|
+
blank=True,
|
78
|
+
help_text="Email subject"
|
79
|
+
)
|
80
|
+
body_text = models.TextField(
|
81
|
+
null=True,
|
82
|
+
blank=True,
|
83
|
+
help_text="Plain text body"
|
84
|
+
)
|
85
|
+
body_html = models.TextField(
|
86
|
+
null=True,
|
87
|
+
blank=True,
|
88
|
+
help_text="HTML body"
|
89
|
+
)
|
90
|
+
|
91
|
+
# Template support (simple; FK can be added later)
|
92
|
+
template_name = models.CharField(
|
93
|
+
max_length=255,
|
94
|
+
null=True,
|
95
|
+
blank=True,
|
96
|
+
help_text="Optional EmailTemplate name used to render this message"
|
97
|
+
)
|
98
|
+
template_context = models.JSONField(
|
99
|
+
default=dict,
|
100
|
+
blank=True,
|
101
|
+
help_text="Context used when rendering a template"
|
102
|
+
)
|
103
|
+
|
104
|
+
# Delivery status
|
105
|
+
status = models.CharField(
|
106
|
+
max_length=32,
|
107
|
+
choices=STATUS_CHOICES,
|
108
|
+
default=STATUS_QUEUED,
|
109
|
+
db_index=True,
|
110
|
+
help_text="Current delivery status"
|
111
|
+
)
|
112
|
+
status_reason = models.TextField(
|
113
|
+
null=True,
|
114
|
+
blank=True,
|
115
|
+
help_text="Details or raw payload for bounces/complaints/errors"
|
116
|
+
)
|
117
|
+
|
118
|
+
metadata = models.JSONField(
|
119
|
+
default=dict,
|
120
|
+
blank=True,
|
121
|
+
help_text="Arbitrary metadata for downstream processing/auditing"
|
122
|
+
)
|
123
|
+
|
124
|
+
class Meta:
|
125
|
+
db_table = "aws_sent_message"
|
126
|
+
indexes = [
|
127
|
+
models.Index(fields=["modified"]),
|
128
|
+
models.Index(fields=["status"]),
|
129
|
+
models.Index(fields=["ses_message_id"]),
|
130
|
+
]
|
131
|
+
ordering = ["-created", "id"]
|
132
|
+
|
133
|
+
class RestMeta:
|
134
|
+
VIEW_PERMS = ["manage_aws"]
|
135
|
+
SAVE_PERMS = ["manage_aws"]
|
136
|
+
DELETE_PERMS = ["manage_aws"]
|
137
|
+
SEARCH_FIELDS = ["subject", "ses_message_id"]
|
138
|
+
GRAPHS = {
|
139
|
+
"basic": {
|
140
|
+
"fields": [
|
141
|
+
"id",
|
142
|
+
"mailbox",
|
143
|
+
"ses_message_id",
|
144
|
+
"subject",
|
145
|
+
"to_addresses",
|
146
|
+
"status",
|
147
|
+
"created",
|
148
|
+
],
|
149
|
+
"graphs": {"mailbox": "basic"}
|
150
|
+
},
|
151
|
+
"default": {
|
152
|
+
"fields": [
|
153
|
+
"id",
|
154
|
+
"mailbox",
|
155
|
+
"ses_message_id",
|
156
|
+
"to_addresses",
|
157
|
+
"cc_addresses",
|
158
|
+
"bcc_addresses",
|
159
|
+
"subject",
|
160
|
+
"body_text",
|
161
|
+
"body_html",
|
162
|
+
"template_name",
|
163
|
+
"template_context",
|
164
|
+
"status",
|
165
|
+
"status_reason",
|
166
|
+
"metadata",
|
167
|
+
"created",
|
168
|
+
"modified",
|
169
|
+
],
|
170
|
+
"graphs": {"mailbox": "basic"}
|
171
|
+
},
|
172
|
+
}
|
173
|
+
|
174
|
+
def __str__(self) -> str:
|
175
|
+
return self.subject or self.ses_message_id or f"SentMessage {self.pk}"
|
mojo/apps/aws/rest/__init__.py
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
from mojo import decorators as md
|
2
|
+
from mojo.apps.aws.models import EmailDomain, Mailbox
|
3
|
+
|
4
|
+
|
5
|
+
"""
|
6
|
+
AWS Email REST Handlers
|
7
|
+
|
8
|
+
Endpoints:
|
9
|
+
- Domain CRUD:
|
10
|
+
- GET/POST/PUT/DELETE /aws/email/domain
|
11
|
+
- GET/POST/PUT/DELETE /aws/email/domain/<int:pk>
|
12
|
+
|
13
|
+
- Mailbox CRUD:
|
14
|
+
- GET/POST/PUT/DELETE /aws/email/mailbox
|
15
|
+
- GET/POST/PUT/DELETE /aws/email/mailbox/<int:pk>
|
16
|
+
|
17
|
+
These handlers delegate to the models' on_rest_request, which uses RestMeta for
|
18
|
+
permission checks, graphs, and default CRUD behavior.
|
19
|
+
"""
|
20
|
+
|
21
|
+
|
22
|
+
@md.URL('email/domain')
|
23
|
+
@md.URL('email/domain/<int:pk>')
|
24
|
+
@md.requires_perms("manage_aws")
|
25
|
+
def on_email_domain(request, pk=None):
|
26
|
+
return EmailDomain.on_rest_request(request, pk)
|
27
|
+
|
28
|
+
|
29
|
+
@md.URL('email/mailbox')
|
30
|
+
@md.URL('email/mailbox/<int:pk>')
|
31
|
+
@md.requires_perms("manage_aws")
|
32
|
+
def on_mailbox(request, pk=None):
|
33
|
+
return Mailbox.on_rest_request(request, pk)
|
@@ -0,0 +1,183 @@
|
|
1
|
+
from typing import Dict, Any
|
2
|
+
|
3
|
+
from mojo import decorators as md
|
4
|
+
from mojo import JsonResponse
|
5
|
+
from mojo.helpers import logit
|
6
|
+
|
7
|
+
# Use the new email_ops service
|
8
|
+
from mojo.apps.aws.services.email_ops import (
|
9
|
+
onboard_email_domain,
|
10
|
+
audit_email_domain,
|
11
|
+
reconcile_email_domain,
|
12
|
+
generate_audit_recommendations,
|
13
|
+
EmailDomainNotFound,
|
14
|
+
InvalidConfiguration,
|
15
|
+
)
|
16
|
+
|
17
|
+
logger = logit.get_logger("email", "email.log")
|
18
|
+
|
19
|
+
|
20
|
+
def _get_json(request) -> Dict[str, Any]:
|
21
|
+
return getattr(request, "DATA", {}) or {}
|
22
|
+
|
23
|
+
|
24
|
+
@md.URL("email/domain/<int:pk>/onboard")
|
25
|
+
@md.requires_perms("manage_aws")
|
26
|
+
def on_email_domain_onboard(request, pk: int):
|
27
|
+
"""
|
28
|
+
Kick off domain onboarding:
|
29
|
+
- Request SES domain verification + DKIM tokens
|
30
|
+
- Compute required DNS records (manual or automated via GoDaddy if requested)
|
31
|
+
- Ensure SNS topics + notification mappings
|
32
|
+
- Optionally enable receiving (catch-all → S3 + SNS)
|
33
|
+
- Optionally enable MAIL FROM (returns DNS to add)
|
34
|
+
"""
|
35
|
+
if request.method != "POST":
|
36
|
+
return JsonResponse({"error": "Method not allowed"}, status=405)
|
37
|
+
|
38
|
+
payload = _get_json(request)
|
39
|
+
|
40
|
+
try:
|
41
|
+
result = onboard_email_domain(
|
42
|
+
domain_pk=pk,
|
43
|
+
region=payload.get("region"),
|
44
|
+
receiving_enabled=payload.get("receiving_enabled"),
|
45
|
+
s3_bucket=payload.get("s3_inbound_bucket"),
|
46
|
+
s3_prefix=payload.get("s3_inbound_prefix"),
|
47
|
+
ensure_mail_from=bool(payload.get("ensure_mail_from", False)),
|
48
|
+
mail_from_subdomain=payload.get("mail_from_subdomain", "feedback"),
|
49
|
+
dns_mode=payload.get("dns_mode"),
|
50
|
+
endpoints=payload.get("endpoints") or {
|
51
|
+
"bounce": payload.get("bounce_endpoint"),
|
52
|
+
"complaint": payload.get("complaint_endpoint"),
|
53
|
+
"delivery": payload.get("delivery_endpoint"),
|
54
|
+
"inbound": payload.get("inbound_endpoint"),
|
55
|
+
},
|
56
|
+
access_key=payload.get("aws_access_key"),
|
57
|
+
secret_key=payload.get("aws_secret_key"),
|
58
|
+
godaddy_key=payload.get("godaddy_key"),
|
59
|
+
godaddy_secret=payload.get("godaddy_secret"),
|
60
|
+
)
|
61
|
+
|
62
|
+
return JsonResponse({
|
63
|
+
"status": True,
|
64
|
+
"data": {
|
65
|
+
"domain": result.domain,
|
66
|
+
"region": result.region,
|
67
|
+
"dns_records": result.dns_records,
|
68
|
+
"dkim_tokens": result.dkim_tokens,
|
69
|
+
"topic_arns": result.topic_arns,
|
70
|
+
"receipt_rule": result.receipt_rule,
|
71
|
+
"rule_set": result.rule_set,
|
72
|
+
"notes": result.notes,
|
73
|
+
}
|
74
|
+
})
|
75
|
+
except EmailDomainNotFound:
|
76
|
+
return JsonResponse({"error": "EmailDomain not found", "code": 404}, status=404)
|
77
|
+
except InvalidConfiguration as e:
|
78
|
+
return JsonResponse({"error": str(e)}, status=400)
|
79
|
+
except Exception as e:
|
80
|
+
logger.error(f"onboard error for domain pk={pk}: {e}")
|
81
|
+
return JsonResponse({"error": str(e)}, status=500)
|
82
|
+
|
83
|
+
|
84
|
+
@md.URL("email/domain/<int:pk>/audit")
|
85
|
+
@md.requires_perms("manage_aws")
|
86
|
+
def on_email_domain_audit(request, pk: int):
|
87
|
+
"""
|
88
|
+
Audit SES/SNS/S3 configuration for the domain and return a drift report.
|
89
|
+
Uses the model configuration to compute desired receiving.
|
90
|
+
"""
|
91
|
+
if request.method not in ("GET", "POST"):
|
92
|
+
return JsonResponse({"error": "Method not allowed"}, status=405)
|
93
|
+
|
94
|
+
payload = _get_json(request) if request.method == "POST" else {}
|
95
|
+
|
96
|
+
try:
|
97
|
+
result = audit_email_domain(
|
98
|
+
domain_pk=pk,
|
99
|
+
region=payload.get("region"),
|
100
|
+
access_key=payload.get("aws_access_key"),
|
101
|
+
secret_key=payload.get("aws_secret_key"),
|
102
|
+
rule_set=payload.get("rule_set"),
|
103
|
+
rule_name=payload.get("rule_name"),
|
104
|
+
)
|
105
|
+
|
106
|
+
return JsonResponse({
|
107
|
+
"status": True,
|
108
|
+
"data": {
|
109
|
+
"domain": result.domain,
|
110
|
+
"region": result.region,
|
111
|
+
"status": result.status,
|
112
|
+
"audit_pass": result.audit_pass,
|
113
|
+
"checks": result.checks,
|
114
|
+
"items": [
|
115
|
+
{
|
116
|
+
"resource": it.resource,
|
117
|
+
"desired": it.desired,
|
118
|
+
"current": it.current,
|
119
|
+
"status": it.status
|
120
|
+
} for it in result.items
|
121
|
+
],
|
122
|
+
"recommendations": generate_audit_recommendations(result.report)
|
123
|
+
}
|
124
|
+
})
|
125
|
+
except EmailDomainNotFound:
|
126
|
+
return JsonResponse({"error": "EmailDomain not found", "code": 404}, status=404)
|
127
|
+
except Exception as e:
|
128
|
+
logger.error(f"audit error for domain pk={pk}: {e}")
|
129
|
+
return JsonResponse({"error": str(e)}, status=500)
|
130
|
+
|
131
|
+
|
132
|
+
@md.URL("email/domain/<int:pk>/reconcile")
|
133
|
+
@md.requires_perms("manage_aws")
|
134
|
+
def on_email_domain_reconcile(request, pk: int):
|
135
|
+
"""
|
136
|
+
Attempt to reconcile SES/SNS for the domain:
|
137
|
+
- Ensure SNS topics and notification mappings
|
138
|
+
- Ensure receiving catch-all rule (if receiving_enabled)
|
139
|
+
- Optionally configure MAIL FROM
|
140
|
+
Does not modify DNS; use onboarding + DNS mode or apply manually.
|
141
|
+
"""
|
142
|
+
if request.method != "POST":
|
143
|
+
return JsonResponse({"error": "Method not allowed"}, status=405)
|
144
|
+
|
145
|
+
payload = _get_json(request)
|
146
|
+
|
147
|
+
try:
|
148
|
+
result = reconcile_email_domain(
|
149
|
+
domain_pk=pk,
|
150
|
+
region=payload.get("region"),
|
151
|
+
receiving_enabled=payload.get("receiving_enabled"),
|
152
|
+
s3_bucket=payload.get("s3_inbound_bucket"),
|
153
|
+
s3_prefix=payload.get("s3_inbound_prefix"),
|
154
|
+
ensure_mail_from=bool(payload.get("ensure_mail_from", False)),
|
155
|
+
mail_from_subdomain=payload.get("mail_from_subdomain", "feedback"),
|
156
|
+
endpoints=payload.get("endpoints") or {
|
157
|
+
"bounce": payload.get("bounce_endpoint"),
|
158
|
+
"complaint": payload.get("complaint_endpoint"),
|
159
|
+
"delivery": payload.get("delivery_endpoint"),
|
160
|
+
"inbound": payload.get("inbound_endpoint"),
|
161
|
+
},
|
162
|
+
access_key=payload.get("aws_access_key"),
|
163
|
+
secret_key=payload.get("aws_secret_key"),
|
164
|
+
)
|
165
|
+
|
166
|
+
return JsonResponse({
|
167
|
+
"status": True,
|
168
|
+
"data": {
|
169
|
+
"domain": result.domain,
|
170
|
+
"region": result.region,
|
171
|
+
"topic_arns": result.topic_arns,
|
172
|
+
"receipt_rule": result.receipt_rule,
|
173
|
+
"rule_set": result.rule_set,
|
174
|
+
"notes": result.notes,
|
175
|
+
}
|
176
|
+
})
|
177
|
+
except EmailDomainNotFound:
|
178
|
+
return JsonResponse({"error": "EmailDomain not found", "code": 404}, status=404)
|
179
|
+
except InvalidConfiguration as e:
|
180
|
+
return JsonResponse({"error": str(e)}, status=400)
|
181
|
+
except Exception as e:
|
182
|
+
logger.error(f"reconcile error for domain pk={pk}: {e}")
|
183
|
+
return JsonResponse({"error": str(e)}, status=500)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from mojo import decorators as md
|
2
|
+
from mojo.apps.aws.models import IncomingEmail, SentMessage
|
3
|
+
|
4
|
+
"""
|
5
|
+
AWS Email Messages REST Handlers
|
6
|
+
|
7
|
+
Endpoints:
|
8
|
+
- Incoming emails (list/detail via model.on_rest_request):
|
9
|
+
- GET/POST/PUT/DELETE /email/incoming
|
10
|
+
- GET/POST/PUT/DELETE /email/incoming/<int:pk>
|
11
|
+
|
12
|
+
- Sent messages (list/detail via model.on_rest_request):
|
13
|
+
- GET/POST/PUT/DELETE /email/sent
|
14
|
+
- GET/POST/PUT/DELETE /email/sent/<int:pk>
|
15
|
+
|
16
|
+
All endpoints require the "manage_aws" permission and delegate to the models' on_rest_request
|
17
|
+
for CRUD operations, leveraging RestMeta permissions and graphs.
|
18
|
+
"""
|
19
|
+
|
20
|
+
|
21
|
+
@md.URL('email/incoming')
|
22
|
+
@md.URL('email/incoming/<int:pk>')
|
23
|
+
@md.requires_perms("manage_aws")
|
24
|
+
def on_incoming_email(request, pk=None):
|
25
|
+
return IncomingEmail.on_rest_request(request, pk)
|
26
|
+
|
27
|
+
|
28
|
+
@md.URL('email/sent')
|
29
|
+
@md.URL('email/sent/<int:pk>')
|
30
|
+
@md.requires_perms("manage_aws")
|
31
|
+
def on_sent_message(request, pk=None):
|
32
|
+
return SentMessage.on_rest_request(request, pk)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
from mojo.apps.aws.services import send_template_email
|
2
|
+
from typing import Any, Dict, List, Optional
|
3
|
+
|
4
|
+
from mojo import decorators as md
|
5
|
+
from mojo import JsonResponse
|
6
|
+
from mojo.apps.aws.models import Mailbox, SentMessage, EmailDomain, EmailTemplate
|
7
|
+
from mojo.helpers.aws.ses import EmailSender
|
8
|
+
from mojo.helpers.settings import settings
|
9
|
+
from mojo.helpers import logit
|
10
|
+
|
11
|
+
logger = logit.get_logger("email", "email.log")
|
12
|
+
|
13
|
+
|
14
|
+
def _as_list(value: Any) -> List[str]:
|
15
|
+
if value is None:
|
16
|
+
return []
|
17
|
+
if isinstance(value, (list, tuple)):
|
18
|
+
return [str(v).strip() for v in value if str(v).strip()]
|
19
|
+
return [str(value).strip()] if str(value).strip() else []
|
20
|
+
|
21
|
+
|
22
|
+
@md.URL("email/send")
|
23
|
+
@md.requires_perms("manage_aws")
|
24
|
+
def on_send_email(request):
|
25
|
+
"""
|
26
|
+
Send an email through AWS SES using a Mailbox resolved by from_email.
|
27
|
+
|
28
|
+
Request (POST JSON):
|
29
|
+
{
|
30
|
+
"from_email": "support@example.com", // required, resolves Mailbox
|
31
|
+
"to": ["user@example.org"], // required (list or string)
|
32
|
+
"cc": [], // optional
|
33
|
+
"bcc": [], // optional
|
34
|
+
"subject": "Hello", // required if not using template_name
|
35
|
+
"body_text": "Text body", // optional
|
36
|
+
"body_html": "<p>HTML body</p>", // optional
|
37
|
+
"reply_to": ["replies@example.com"], // optional
|
38
|
+
"template_name": "db-template-optional", // optional, uses DB EmailTemplate if provided
|
39
|
+
"ses_template_name": "ses-template-optional", // optional, uses AWS SES managed template
|
40
|
+
"template_context": { ... }, // optional, for DB/SES template context
|
41
|
+
"aws_access_key": "...", // optional, defaults to settings
|
42
|
+
"aws_secret_key": "...", // optional, defaults to settings
|
43
|
+
"allow_unverified": false // optional, allow send even if domain.status != 'verified'
|
44
|
+
}
|
45
|
+
|
46
|
+
Behavior:
|
47
|
+
- Resolves the Mailbox by from_email (case-insensitive).
|
48
|
+
- Ensures mailbox.allow_outbound is True.
|
49
|
+
- Uses mailbox.domain.region (or settings.AWS_REGION) to send via SES.
|
50
|
+
- If template_name is provided and matches a DB EmailTemplate, renders and uses EmailSender.send_email with the rendered subject/body.
|
51
|
+
If ses_template_name is provided, uses EmailSender.send_template_email (AWS SES managed template).
|
52
|
+
Otherwise uses EmailSender.send_email with subject/body_text/body_html.
|
53
|
+
- Creates a SentMessage row and updates with SES MessageId and status.
|
54
|
+
"""
|
55
|
+
if request.method != "POST":
|
56
|
+
return JsonResponse({"error": "Method not allowed"}, status=405)
|
57
|
+
|
58
|
+
data: Dict[str, Any] = getattr(request, "DATA", {}) or {}
|
59
|
+
|
60
|
+
from_email = (data.get("from_email") or "").strip()
|
61
|
+
if not from_email:
|
62
|
+
return JsonResponse({"error": "from_email is required"}, status=400)
|
63
|
+
|
64
|
+
# Resolve Mailbox by email (case-insensitive)
|
65
|
+
mailbox = Mailbox.objects.select_related("domain").filter(email__iexact=from_email).first()
|
66
|
+
if not mailbox:
|
67
|
+
return JsonResponse({"error": f"Mailbox not found for from_email={from_email}", "code": 404}, status=404)
|
68
|
+
|
69
|
+
if not mailbox.allow_outbound:
|
70
|
+
return JsonResponse({"error": "Outbound sending is disabled for this mailbox", "code": 403}, status=403)
|
71
|
+
|
72
|
+
|
73
|
+
to = _as_list(data.get("to"))
|
74
|
+
cc = _as_list(data.get("cc"))
|
75
|
+
bcc = _as_list(data.get("bcc"))
|
76
|
+
reply_to = _as_list(data.get("reply_to")) or [from_email]
|
77
|
+
|
78
|
+
if not to:
|
79
|
+
return JsonResponse({"error": "At least one recipient in 'to' is required"}, status=400)
|
80
|
+
|
81
|
+
subject = (data.get("subject") or "").strip()
|
82
|
+
body_text = data.get("body_text")
|
83
|
+
body_html = data.get("body_html")
|
84
|
+
template_name = (data.get("template_name") or "").strip() or None
|
85
|
+
template_context = data.get("template_context") or {}
|
86
|
+
|
87
|
+
if template_name:
|
88
|
+
res = mailbox.send_template_email(
|
89
|
+
to, template_name, template_context,
|
90
|
+
cc, bcc, reply_to)
|
91
|
+
else:
|
92
|
+
res = mailbox.send_email(
|
93
|
+
to=to,
|
94
|
+
subject=subject,
|
95
|
+
body_text=body_text,
|
96
|
+
body_html=body_html,
|
97
|
+
cc=cc,
|
98
|
+
bcc=bcc,
|
99
|
+
reply_to=reply_to
|
100
|
+
)
|
101
|
+
return res.on_rest_get(request)
|