django-nativemojo 0.1.15__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.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
- django_nativemojo-0.1.16.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 +281 -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.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.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,132 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from django.template import Template, Context
|
3
|
+
from mojo.models import MojoModel
|
4
|
+
|
5
|
+
|
6
|
+
class EmailTemplate(models.Model, MojoModel):
|
7
|
+
"""
|
8
|
+
EmailTemplate
|
9
|
+
|
10
|
+
Stores Django template strings for subject, HTML, and plain text bodies.
|
11
|
+
Used to render outbound emails with templating support.
|
12
|
+
|
13
|
+
Rendering:
|
14
|
+
- Call `render_all(context)` to render subject, text, and html.
|
15
|
+
- Any of the template fields can be blank; rendering will return None for blanks.
|
16
|
+
|
17
|
+
Notes:
|
18
|
+
- Locale/i18n variations can be added later by introducing a related table or locale key.
|
19
|
+
- This model does not send emails itself; use the email service or REST endpoint
|
20
|
+
to send using rendered content.
|
21
|
+
"""
|
22
|
+
|
23
|
+
created = models.DateTimeField(auto_now_add=True, editable=False)
|
24
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
25
|
+
|
26
|
+
name = models.CharField(
|
27
|
+
max_length=255,
|
28
|
+
unique=True,
|
29
|
+
db_index=True,
|
30
|
+
help_text="Unique template name (used by callers to reference this template)"
|
31
|
+
)
|
32
|
+
|
33
|
+
subject_template = models.TextField(
|
34
|
+
blank=True,
|
35
|
+
default="",
|
36
|
+
help_text="Django template string for the email subject"
|
37
|
+
)
|
38
|
+
html_template = models.TextField(
|
39
|
+
blank=True,
|
40
|
+
default="",
|
41
|
+
help_text="Django template string for the HTML body"
|
42
|
+
)
|
43
|
+
text_template = models.TextField(
|
44
|
+
blank=True,
|
45
|
+
default="",
|
46
|
+
help_text="Django template string for the plain text body"
|
47
|
+
)
|
48
|
+
|
49
|
+
metadata = models.JSONField(
|
50
|
+
default=dict,
|
51
|
+
blank=True,
|
52
|
+
help_text="Arbitrary metadata for this template (e.g., description, tags)"
|
53
|
+
)
|
54
|
+
|
55
|
+
class Meta:
|
56
|
+
db_table = "aws_email_template"
|
57
|
+
indexes = [
|
58
|
+
models.Index(fields=["modified"]),
|
59
|
+
models.Index(fields=["name"]),
|
60
|
+
]
|
61
|
+
ordering = ["name"]
|
62
|
+
|
63
|
+
class RestMeta:
|
64
|
+
VIEW_PERMS = ["manage_aws"]
|
65
|
+
SAVE_PERMS = ["manage_aws"]
|
66
|
+
DELETE_PERMS = ["manage_aws"]
|
67
|
+
SEARCH_FIELDS = ["name"]
|
68
|
+
GRAPHS = {
|
69
|
+
"basic": {
|
70
|
+
"fields": [
|
71
|
+
"id",
|
72
|
+
"name",
|
73
|
+
"created",
|
74
|
+
"modified",
|
75
|
+
]
|
76
|
+
},
|
77
|
+
"default": {
|
78
|
+
"fields": [
|
79
|
+
"id",
|
80
|
+
"name",
|
81
|
+
"subject_template",
|
82
|
+
"html_template",
|
83
|
+
"text_template",
|
84
|
+
"metadata",
|
85
|
+
"created",
|
86
|
+
"modified",
|
87
|
+
]
|
88
|
+
},
|
89
|
+
}
|
90
|
+
|
91
|
+
def __str__(self) -> str:
|
92
|
+
return self.name
|
93
|
+
|
94
|
+
# --- Rendering helpers -------------------------------------------------
|
95
|
+
|
96
|
+
@staticmethod
|
97
|
+
def _render_template(tpl_str: str, context: dict | None) -> str | None:
|
98
|
+
"""
|
99
|
+
Render a Django template string with the provided context.
|
100
|
+
Returns None if tpl_str is empty.
|
101
|
+
"""
|
102
|
+
if not tpl_str:
|
103
|
+
return None
|
104
|
+
# Using Django's Template/Context is sufficient for inline strings.
|
105
|
+
# For more advanced usage or custom engines, we can wire in django.template.engines.
|
106
|
+
try:
|
107
|
+
tpl = Template(tpl_str)
|
108
|
+
ctx = Context(context or {})
|
109
|
+
return tpl.render(ctx)
|
110
|
+
except Exception as e:
|
111
|
+
# We deliberately re-raise to let callers decide how to handle failures.
|
112
|
+
raise e
|
113
|
+
|
114
|
+
def render_subject(self, context: dict | None = None) -> str | None:
|
115
|
+
return self._render_template(self.subject_template, context)
|
116
|
+
|
117
|
+
def render_html(self, context: dict | None = None) -> str | None:
|
118
|
+
return self._render_template(self.html_template, context)
|
119
|
+
|
120
|
+
def render_text(self, context: dict | None = None) -> str | None:
|
121
|
+
return self._render_template(self.text_template, context)
|
122
|
+
|
123
|
+
def render_all(self, context: dict | None = None) -> dict:
|
124
|
+
"""
|
125
|
+
Render subject, text, and html templates with the provided context.
|
126
|
+
Returns a dict with keys: subject, text, html (values can be None).
|
127
|
+
"""
|
128
|
+
return {
|
129
|
+
"subject": self.render_subject(context),
|
130
|
+
"text": self.render_text(context),
|
131
|
+
"html": self.render_html(context),
|
132
|
+
}
|
@@ -0,0 +1,197 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class IncomingEmail(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
IncomingEmail
|
8
|
+
|
9
|
+
Represents a single inbound email received via SES and stored in S3.
|
10
|
+
Raw MIME is stored in S3 (s3_object_url). Parsed metadata and content are
|
11
|
+
stored in this model. Attachments are stored in the same inbound S3 bucket
|
12
|
+
and represented in a separate model (EmailAttachment, added later).
|
13
|
+
|
14
|
+
Routing:
|
15
|
+
- This model may be associated to a Mailbox if any recipient matches.
|
16
|
+
- An async handler (on the Mailbox) can process the message after creation.
|
17
|
+
"""
|
18
|
+
|
19
|
+
created = models.DateTimeField(auto_now_add=True, editable=False)
|
20
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
21
|
+
|
22
|
+
mailbox = models.ForeignKey(
|
23
|
+
"aws.Mailbox",
|
24
|
+
null=True,
|
25
|
+
blank=True,
|
26
|
+
related_name="incoming_emails",
|
27
|
+
on_delete=models.CASCADE,
|
28
|
+
help_text="Associated mailbox if any recipient matches"
|
29
|
+
)
|
30
|
+
|
31
|
+
# Storage and identity
|
32
|
+
s3_object_url = models.CharField(
|
33
|
+
max_length=512,
|
34
|
+
help_text="S3 URL for the raw MIME message (e.g., s3://bucket/key)"
|
35
|
+
)
|
36
|
+
message_id = models.CharField(
|
37
|
+
max_length=255,
|
38
|
+
null=True,
|
39
|
+
blank=True,
|
40
|
+
db_index=True,
|
41
|
+
help_text="SMTP Message-ID header (if present)"
|
42
|
+
)
|
43
|
+
|
44
|
+
# Headers and addressing
|
45
|
+
from_address = models.CharField(
|
46
|
+
max_length=512,
|
47
|
+
null=True,
|
48
|
+
blank=True,
|
49
|
+
help_text="Raw From header address (may include name)"
|
50
|
+
)
|
51
|
+
to_addresses = models.JSONField(
|
52
|
+
default=list,
|
53
|
+
blank=True,
|
54
|
+
help_text="List of recipient addresses from To header"
|
55
|
+
)
|
56
|
+
cc_addresses = models.JSONField(
|
57
|
+
default=list,
|
58
|
+
blank=True,
|
59
|
+
help_text="List of recipient addresses from Cc header"
|
60
|
+
)
|
61
|
+
subject = models.CharField(
|
62
|
+
max_length=512,
|
63
|
+
null=True,
|
64
|
+
blank=True,
|
65
|
+
help_text="Email subject"
|
66
|
+
)
|
67
|
+
date_header = models.DateTimeField(
|
68
|
+
null=True,
|
69
|
+
blank=True,
|
70
|
+
help_text="Parsed Date header from the message"
|
71
|
+
)
|
72
|
+
headers = models.JSONField(
|
73
|
+
default=dict,
|
74
|
+
blank=True,
|
75
|
+
help_text="All headers as a JSON object (flattened)"
|
76
|
+
)
|
77
|
+
|
78
|
+
# Content
|
79
|
+
text_body = models.TextField(
|
80
|
+
null=True,
|
81
|
+
blank=True,
|
82
|
+
help_text="Extracted plain text body (if available)"
|
83
|
+
)
|
84
|
+
html_body = models.TextField(
|
85
|
+
null=True,
|
86
|
+
blank=True,
|
87
|
+
help_text="Extracted HTML body (if available)"
|
88
|
+
)
|
89
|
+
|
90
|
+
# Misc
|
91
|
+
size_bytes = models.IntegerField(
|
92
|
+
default=0,
|
93
|
+
help_text="Approximate size of the raw message in bytes"
|
94
|
+
)
|
95
|
+
received_at = models.DateTimeField(
|
96
|
+
null=True,
|
97
|
+
blank=True,
|
98
|
+
db_index=True,
|
99
|
+
help_text="Time message was received (from SNS/S3 event or set by parser)"
|
100
|
+
)
|
101
|
+
|
102
|
+
# Processing status
|
103
|
+
processed = models.BooleanField(
|
104
|
+
default=False,
|
105
|
+
help_text="True if post-receive processing completed"
|
106
|
+
)
|
107
|
+
process_status = models.CharField(
|
108
|
+
max_length=32,
|
109
|
+
default="pending",
|
110
|
+
db_index=True,
|
111
|
+
help_text="Processing status: pending | success | error"
|
112
|
+
)
|
113
|
+
process_error = models.TextField(
|
114
|
+
null=True,
|
115
|
+
blank=True,
|
116
|
+
help_text="Error details if processing failed"
|
117
|
+
)
|
118
|
+
|
119
|
+
class Meta:
|
120
|
+
db_table = "aws_incoming_email"
|
121
|
+
indexes = [
|
122
|
+
models.Index(fields=["modified"]),
|
123
|
+
models.Index(fields=["received_at"]),
|
124
|
+
models.Index(fields=["message_id"]),
|
125
|
+
]
|
126
|
+
ordering = ["-received_at", "-created"]
|
127
|
+
|
128
|
+
class RestMeta:
|
129
|
+
VIEW_PERMS = ["manage_aws"]
|
130
|
+
SAVE_PERMS = ["manage_aws"]
|
131
|
+
DELETE_PERMS = ["manage_aws"]
|
132
|
+
SEARCH_FIELDS = ["subject", "from_address", "message_id"]
|
133
|
+
GRAPHS = {
|
134
|
+
"basic": {
|
135
|
+
"fields": [
|
136
|
+
"id",
|
137
|
+
"mailbox",
|
138
|
+
"subject",
|
139
|
+
"from_address",
|
140
|
+
"to_addresses",
|
141
|
+
"received_at",
|
142
|
+
"processed",
|
143
|
+
"process_status",
|
144
|
+
"created",
|
145
|
+
],
|
146
|
+
"graphs": {"mailbox": "basic"}
|
147
|
+
},
|
148
|
+
"default": {
|
149
|
+
"fields": [
|
150
|
+
"id",
|
151
|
+
"mailbox",
|
152
|
+
"s3_object_url",
|
153
|
+
"message_id",
|
154
|
+
"from_address",
|
155
|
+
"to_addresses",
|
156
|
+
"cc_addresses",
|
157
|
+
"subject",
|
158
|
+
"date_header",
|
159
|
+
"headers",
|
160
|
+
"size_bytes",
|
161
|
+
"received_at",
|
162
|
+
"processed",
|
163
|
+
"process_status",
|
164
|
+
"process_error",
|
165
|
+
"created",
|
166
|
+
"modified",
|
167
|
+
],
|
168
|
+
"graphs": {"mailbox": "basic"}
|
169
|
+
},
|
170
|
+
"full": {
|
171
|
+
"fields": [
|
172
|
+
"id",
|
173
|
+
"mailbox",
|
174
|
+
"s3_object_url",
|
175
|
+
"message_id",
|
176
|
+
"from_address",
|
177
|
+
"to_addresses",
|
178
|
+
"cc_addresses",
|
179
|
+
"subject",
|
180
|
+
"date_header",
|
181
|
+
"headers",
|
182
|
+
"text_body",
|
183
|
+
"html_body",
|
184
|
+
"size_bytes",
|
185
|
+
"received_at",
|
186
|
+
"processed",
|
187
|
+
"process_status",
|
188
|
+
"process_error",
|
189
|
+
"created",
|
190
|
+
"modified",
|
191
|
+
],
|
192
|
+
"graphs": {"mailbox": "basic"}
|
193
|
+
},
|
194
|
+
}
|
195
|
+
|
196
|
+
def __str__(self) -> str:
|
197
|
+
return self.subject or self.message_id or f"IncomingEmail {self.pk}"
|
@@ -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
|
+
)
|