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,288 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from django.core.exceptions import ValidationError
|
3
|
+
from mojo.models import MojoModel
|
4
|
+
from typing import Optional, Union, Sequence, Dict, Any
|
5
|
+
|
6
|
+
|
7
|
+
class Mailbox(models.Model, MojoModel):
|
8
|
+
"""
|
9
|
+
Mailbox
|
10
|
+
|
11
|
+
Minimal model representing a single email address (mailbox) within a verified EmailDomain.
|
12
|
+
Sending and receiving policies are configured per mailbox. When inbound messages arrive
|
13
|
+
(domain-level catch-all), they are routed to the matching mailbox by recipient address and
|
14
|
+
optionally dispatched to an async handler.
|
15
|
+
|
16
|
+
Notes:
|
17
|
+
- `email` is the full email address (e.g., support@example.com) and is unique.
|
18
|
+
- `domain` references the owning EmailDomain (e.g., example.com).
|
19
|
+
- `allow_inbound` and `allow_outbound` control behavior for this mailbox.
|
20
|
+
- `async_handler` is a dotted path "package.module:function" used by the Tasks system.
|
21
|
+
- `metadata` allows flexible extension without schema churn.
|
22
|
+
"""
|
23
|
+
|
24
|
+
created = models.DateTimeField(auto_now_add=True, editable=False)
|
25
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
26
|
+
|
27
|
+
domain = models.ForeignKey(
|
28
|
+
"EmailDomain",
|
29
|
+
related_name="mailboxes",
|
30
|
+
on_delete=models.CASCADE,
|
31
|
+
help_text="Owning email domain (SES identity)"
|
32
|
+
)
|
33
|
+
|
34
|
+
email = models.EmailField(
|
35
|
+
unique=True,
|
36
|
+
db_index=True,
|
37
|
+
help_text="Full email address for this mailbox (e.g., support@example.com)"
|
38
|
+
)
|
39
|
+
|
40
|
+
allow_inbound = models.BooleanField(
|
41
|
+
default=True,
|
42
|
+
help_text="If true, inbound messages addressed to this mailbox will be processed"
|
43
|
+
)
|
44
|
+
allow_outbound = models.BooleanField(
|
45
|
+
default=True,
|
46
|
+
help_text="If true, outbound messages can be sent from this mailbox"
|
47
|
+
)
|
48
|
+
|
49
|
+
async_handler = models.CharField(
|
50
|
+
max_length=255,
|
51
|
+
null=True,
|
52
|
+
blank=True,
|
53
|
+
help_text="Dotted path to async handler: 'package.module:function'"
|
54
|
+
)
|
55
|
+
|
56
|
+
metadata = models.JSONField(default=dict, blank=True)
|
57
|
+
|
58
|
+
is_system_default = models.BooleanField(
|
59
|
+
default=False,
|
60
|
+
db_index=True,
|
61
|
+
help_text="System-wide default mailbox (only one allowed)"
|
62
|
+
)
|
63
|
+
|
64
|
+
is_domain_default = models.BooleanField(
|
65
|
+
default=False,
|
66
|
+
db_index=True,
|
67
|
+
help_text="Default mailbox for this domain (one per domain)"
|
68
|
+
)
|
69
|
+
|
70
|
+
class Meta:
|
71
|
+
db_table = "aws_mailbox"
|
72
|
+
indexes = [
|
73
|
+
models.Index(fields=["modified"]),
|
74
|
+
models.Index(fields=["email"]),
|
75
|
+
models.Index(fields=["is_system_default"]),
|
76
|
+
models.Index(fields=["is_domain_default", "domain"]),
|
77
|
+
]
|
78
|
+
ordering = ["email"]
|
79
|
+
|
80
|
+
class RestMeta:
|
81
|
+
VIEW_PERMS = ["manage_aws"]
|
82
|
+
SAVE_PERMS = ["manage_aws"]
|
83
|
+
DELETE_PERMS = ["manage_aws"]
|
84
|
+
SEARCH_FIELDS = ["email"]
|
85
|
+
GRAPHS = {
|
86
|
+
"basic": {
|
87
|
+
"fields": [
|
88
|
+
"id",
|
89
|
+
"email",
|
90
|
+
"domain",
|
91
|
+
"allow_inbound",
|
92
|
+
"allow_outbound",
|
93
|
+
"is_system_default",
|
94
|
+
"is_domain_default",
|
95
|
+
]
|
96
|
+
},
|
97
|
+
"default": {
|
98
|
+
"fields": [
|
99
|
+
"id",
|
100
|
+
"email",
|
101
|
+
"domain",
|
102
|
+
"allow_inbound",
|
103
|
+
"allow_outbound",
|
104
|
+
"async_handler",
|
105
|
+
"metadata",
|
106
|
+
"is_system_default",
|
107
|
+
"is_domain_default",
|
108
|
+
"created",
|
109
|
+
"modified",
|
110
|
+
],
|
111
|
+
"graphs": {
|
112
|
+
"domain": "basic"
|
113
|
+
}
|
114
|
+
},
|
115
|
+
}
|
116
|
+
|
117
|
+
def __str__(self) -> str:
|
118
|
+
return self.email
|
119
|
+
|
120
|
+
def clean(self):
|
121
|
+
"""
|
122
|
+
Ensure the mailbox email belongs to the associated domain (simple sanity check).
|
123
|
+
"""
|
124
|
+
super().clean()
|
125
|
+
if self.domain and self.email:
|
126
|
+
domain_name = f"@{self.domain.name.lower()}"
|
127
|
+
if not self.email.lower().endswith(domain_name):
|
128
|
+
raise ValidationError(
|
129
|
+
{"email": f"Email must belong to domain '{self.domain.name}'"}
|
130
|
+
)
|
131
|
+
|
132
|
+
def on_rest_saved(self, changed_fields, created):
|
133
|
+
"""Handle default field uniqueness after REST save"""
|
134
|
+
|
135
|
+
# Clear other system defaults if this was just set as system default
|
136
|
+
if 'is_system_default' in changed_fields and self.is_system_default:
|
137
|
+
Mailbox.objects.exclude(pk=self.pk).update(is_system_default=False)
|
138
|
+
|
139
|
+
# Clear other domain defaults if this was just set as domain default
|
140
|
+
if 'is_domain_default' in changed_fields and self.is_domain_default:
|
141
|
+
Mailbox.objects.filter(domain=self.domain).exclude(pk=self.pk).update(is_domain_default=False)
|
142
|
+
|
143
|
+
super().on_rest_saved(changed_fields, created)
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
def get_system_default(cls) -> Optional['Mailbox']:
|
147
|
+
"""Get the system-wide default mailbox"""
|
148
|
+
return cls.objects.filter(is_system_default=True).first()
|
149
|
+
|
150
|
+
@classmethod
|
151
|
+
def get_domain_default(cls, domain: Union[str, 'EmailDomain']) -> Optional['Mailbox']:
|
152
|
+
"""Get the default mailbox for a specific domain
|
153
|
+
|
154
|
+
Args:
|
155
|
+
domain: Either a domain name string or EmailDomain instance
|
156
|
+
"""
|
157
|
+
if isinstance(domain, str):
|
158
|
+
return cls.objects.filter(domain__name__iexact=domain, is_domain_default=True).first()
|
159
|
+
else:
|
160
|
+
return cls.objects.filter(domain=domain, is_domain_default=True).first()
|
161
|
+
|
162
|
+
@classmethod
|
163
|
+
def get_default(cls, domain: Optional[Union[str, 'EmailDomain']] = None, prefer_domain: bool = True) -> Optional['Mailbox']:
|
164
|
+
"""Smart default: try domain default first (if domain provided), then fall back to system default
|
165
|
+
|
166
|
+
Args:
|
167
|
+
domain: Optional domain to look for domain-specific default
|
168
|
+
prefer_domain: If True (default), prefer domain default over system default
|
169
|
+
"""
|
170
|
+
if domain and prefer_domain:
|
171
|
+
domain_default = cls.get_domain_default(domain)
|
172
|
+
if domain_default:
|
173
|
+
return domain_default
|
174
|
+
|
175
|
+
return cls.get_system_default()
|
176
|
+
|
177
|
+
def send_email(
|
178
|
+
self,
|
179
|
+
to: Union[str, Sequence[str]],
|
180
|
+
subject: Optional[str] = None,
|
181
|
+
body_text: Optional[str] = None,
|
182
|
+
body_html: Optional[str] = None,
|
183
|
+
cc: Optional[Union[str, Sequence[str]]] = None,
|
184
|
+
bcc: Optional[Union[str, Sequence[str]]] = None,
|
185
|
+
reply_to: Optional[Union[str, Sequence[str]]] = None,
|
186
|
+
**kwargs
|
187
|
+
) -> 'SentMessage':
|
188
|
+
"""Send plain email from this mailbox
|
189
|
+
|
190
|
+
Args:
|
191
|
+
to: One or more recipient addresses
|
192
|
+
subject: Email subject
|
193
|
+
body_text: Optional plain text body
|
194
|
+
body_html: Optional HTML body
|
195
|
+
cc, bcc, reply_to: Optional addressing
|
196
|
+
**kwargs: Additional arguments passed to email service (allow_unverified, aws_access_key, etc.)
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
SentMessage instance
|
200
|
+
|
201
|
+
Raises:
|
202
|
+
OutboundNotAllowed: If this mailbox has allow_outbound=False
|
203
|
+
"""
|
204
|
+
from mojo.apps.aws.services import email as email_service
|
205
|
+
|
206
|
+
if not self.allow_outbound:
|
207
|
+
raise email_service.OutboundNotAllowed(f"Outbound sending is disabled for mailbox {self.email}")
|
208
|
+
|
209
|
+
aws_access_key = self.domain.aws_key
|
210
|
+
aws_secret_key = self.domain.aws_secret
|
211
|
+
aws_region = self.domain.aws_region
|
212
|
+
|
213
|
+
return email_service.send_email(
|
214
|
+
from_email=self.email,
|
215
|
+
to=to,
|
216
|
+
subject=subject,
|
217
|
+
body_text=body_text,
|
218
|
+
body_html=body_html,
|
219
|
+
cc=cc,
|
220
|
+
bcc=bcc,
|
221
|
+
reply_to=reply_to,
|
222
|
+
aws_access_key=aws_access_key,
|
223
|
+
aws_secret_key=aws_secret_key,
|
224
|
+
region=aws_region,
|
225
|
+
**kwargs
|
226
|
+
)
|
227
|
+
|
228
|
+
def send_template_email(
|
229
|
+
self,
|
230
|
+
to: Union[str, Sequence[str]],
|
231
|
+
template_name: str,
|
232
|
+
context: Optional[Dict[str, Any]] = None,
|
233
|
+
cc: Optional[Union[str, Sequence[str]]] = None,
|
234
|
+
bcc: Optional[Union[str, Sequence[str]]] = None,
|
235
|
+
reply_to: Optional[Union[str, Sequence[str]]] = None,
|
236
|
+
**kwargs
|
237
|
+
) -> 'SentMessage':
|
238
|
+
"""Send email using DB EmailTemplate
|
239
|
+
|
240
|
+
Args:
|
241
|
+
to: One or more recipient addresses
|
242
|
+
template_name: Name of the EmailTemplate in database
|
243
|
+
context: Template context variables
|
244
|
+
cc, bcc, reply_to: Optional addressing
|
245
|
+
**kwargs: Additional arguments passed to email service (allow_unverified, aws_access_key, etc.)
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
SentMessage instance
|
249
|
+
|
250
|
+
Raises:
|
251
|
+
OutboundNotAllowed: If this mailbox has allow_outbound=False
|
252
|
+
ValueError: If template not found
|
253
|
+
|
254
|
+
Note:
|
255
|
+
Automatically checks for domain-specific template overrides.
|
256
|
+
If "{domain.name}.{template_name}" exists, it will be used instead of the base template.
|
257
|
+
"""
|
258
|
+
from mojo.apps.aws.services import email as email_service
|
259
|
+
from mojo.apps.aws.models import EmailTemplate
|
260
|
+
|
261
|
+
if not self.allow_outbound:
|
262
|
+
raise email_service.OutboundNotAllowed(f"Outbound sending is disabled for mailbox {self.email}")
|
263
|
+
|
264
|
+
# Check for domain-specific template override
|
265
|
+
final_template_name = template_name
|
266
|
+
if self.domain and self.domain.name:
|
267
|
+
domain_template_name = f"{self.domain.name}.{template_name}"
|
268
|
+
# Check if domain-specific template exists
|
269
|
+
if EmailTemplate.objects.filter(name=domain_template_name).exists():
|
270
|
+
final_template_name = domain_template_name
|
271
|
+
|
272
|
+
aws_access_key = self.domain.aws_access_key
|
273
|
+
aws_secret_key = self.domain.aws_secret_key
|
274
|
+
aws_region = self.domain.aws_region
|
275
|
+
|
276
|
+
return email_service.send_with_template(
|
277
|
+
from_email=self.email,
|
278
|
+
to=to,
|
279
|
+
template_name=final_template_name,
|
280
|
+
context=context,
|
281
|
+
cc=cc,
|
282
|
+
bcc=bcc,
|
283
|
+
reply_to=reply_to,
|
284
|
+
aws_access_key=aws_access_key,
|
285
|
+
aws_secret_key=aws_secret_key,
|
286
|
+
region=aws_region,
|
287
|
+
**kwargs
|
288
|
+
)
|
@@ -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}"
|
@@ -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)
|