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,21 @@
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-09-06 00:10
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('aws', '0004_s3bucket'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AlterField(
|
14
|
+
model_name='emaildomain',
|
15
|
+
name='region',
|
16
|
+
field=models.CharField(default='us-east-1', help_text='AWS region for SES operations', max_length=64),
|
17
|
+
),
|
18
|
+
migrations.DeleteModel(
|
19
|
+
name='S3Bucket',
|
20
|
+
),
|
21
|
+
]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"""
|
2
|
+
AWS models package exports
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .email_domain import EmailDomain
|
6
|
+
from .mailbox import Mailbox
|
7
|
+
from .incoming_email import IncomingEmail
|
8
|
+
from .email_attachment import EmailAttachment
|
9
|
+
from .sent_message import SentMessage
|
10
|
+
from .email_template import EmailTemplate
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"EmailDomain",
|
14
|
+
"Mailbox",
|
15
|
+
"IncomingEmail",
|
16
|
+
"EmailAttachment",
|
17
|
+
"SentMessage",
|
18
|
+
"EmailTemplate",
|
19
|
+
]
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class EmailAttachment(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
EmailAttachment
|
8
|
+
|
9
|
+
Represents a single attachment extracted from an incoming email. The binary
|
10
|
+
content is stored in the same inbound S3 bucket as the raw message, and
|
11
|
+
referenced by `stored_as` (e.g., s3://bucket/key).
|
12
|
+
|
13
|
+
Relationships:
|
14
|
+
- incoming_email: FK to IncomingEmail
|
15
|
+
"""
|
16
|
+
|
17
|
+
created = models.DateTimeField(auto_now_add=True, editable=False)
|
18
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
19
|
+
|
20
|
+
incoming_email = models.ForeignKey(
|
21
|
+
"aws.IncomingEmail",
|
22
|
+
related_name="attachments",
|
23
|
+
on_delete=models.CASCADE,
|
24
|
+
help_text="The inbound email this attachment belongs to"
|
25
|
+
)
|
26
|
+
|
27
|
+
filename = models.CharField(
|
28
|
+
max_length=512,
|
29
|
+
null=True,
|
30
|
+
blank=True,
|
31
|
+
help_text="Original filename (if provided by the sender)"
|
32
|
+
)
|
33
|
+
|
34
|
+
content_type = models.CharField(
|
35
|
+
max_length=255,
|
36
|
+
null=True,
|
37
|
+
blank=True,
|
38
|
+
help_text="MIME content type (e.g., application/pdf)"
|
39
|
+
)
|
40
|
+
|
41
|
+
size_bytes = models.IntegerField(
|
42
|
+
default=0,
|
43
|
+
help_text="Size of the stored attachment in bytes (approximate)"
|
44
|
+
)
|
45
|
+
|
46
|
+
stored_as = models.CharField(
|
47
|
+
max_length=512,
|
48
|
+
help_text="Storage reference (e.g., s3://bucket/key)"
|
49
|
+
)
|
50
|
+
|
51
|
+
metadata = models.JSONField(
|
52
|
+
default=dict,
|
53
|
+
blank=True,
|
54
|
+
help_text="Arbitrary metadata (e.g., content-id, part headers)"
|
55
|
+
)
|
56
|
+
|
57
|
+
class Meta:
|
58
|
+
db_table = "aws_email_attachment"
|
59
|
+
indexes = [
|
60
|
+
models.Index(fields=["modified"]),
|
61
|
+
models.Index(fields=["filename"]),
|
62
|
+
]
|
63
|
+
ordering = ["-created", "id"]
|
64
|
+
|
65
|
+
class RestMeta:
|
66
|
+
VIEW_PERMS = ["manage_aws"]
|
67
|
+
SAVE_PERMS = ["manage_aws"]
|
68
|
+
DELETE_PERMS = ["manage_aws"]
|
69
|
+
SEARCH_FIELDS = ["filename", "content_type", "stored_as"]
|
70
|
+
GRAPHS = {
|
71
|
+
"basic": {
|
72
|
+
"fields": [
|
73
|
+
"id",
|
74
|
+
"incoming_email",
|
75
|
+
"filename",
|
76
|
+
"content_type",
|
77
|
+
"size_bytes",
|
78
|
+
"created",
|
79
|
+
],
|
80
|
+
"graphs": {"incoming_email": "basic"},
|
81
|
+
},
|
82
|
+
"default": {
|
83
|
+
"fields": [
|
84
|
+
"id",
|
85
|
+
"incoming_email",
|
86
|
+
"filename",
|
87
|
+
"content_type",
|
88
|
+
"size_bytes",
|
89
|
+
"stored_as",
|
90
|
+
"metadata",
|
91
|
+
"created",
|
92
|
+
"modified",
|
93
|
+
],
|
94
|
+
"graphs": {"incoming_email": "basic"},
|
95
|
+
},
|
96
|
+
}
|
97
|
+
|
98
|
+
def __str__(self) -> str:
|
99
|
+
return self.filename or self.stored_as or f"Attachment {self.pk}"
|
@@ -0,0 +1,218 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel, MojoSecrets
|
3
|
+
from mojo.helpers.settings import settings
|
4
|
+
from mojo.helpers.aws.ses_domain import audit_domain_config, reconcile_domain_config
|
5
|
+
from mojo.helpers import aws
|
6
|
+
|
7
|
+
|
8
|
+
class EmailDomain(MojoSecrets, MojoModel):
|
9
|
+
"""
|
10
|
+
EmailDomain
|
11
|
+
|
12
|
+
Minimal model for managing an SES-backed email domain configuration in MOJO.
|
13
|
+
|
14
|
+
Notes:
|
15
|
+
- 'name' is the domain (e.g., example.com).
|
16
|
+
- 'region' defaults to project AWS region (settings.AWS_REGION) or 'us-east-1'.
|
17
|
+
- 'receiving_enabled' toggles domain-level catch-all receiving (SES receipt rules).
|
18
|
+
- 's3_inbound_bucket' and 's3_inbound_prefix' identify where inbound emails are stored.
|
19
|
+
- 'status' is a lightweight lifecycle indicator: pending | verified | error (free-form for now).
|
20
|
+
- 'metadata' allows flexible per-domain extension without schema churn.
|
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(max_length=255, unique=True, db_index=True)
|
27
|
+
region = models.CharField(
|
28
|
+
max_length=64,
|
29
|
+
default=getattr(settings, 'AWS_REGION', 'us-east-1'),
|
30
|
+
help_text="AWS region for SES operations"
|
31
|
+
)
|
32
|
+
|
33
|
+
status = models.CharField(
|
34
|
+
max_length=32,
|
35
|
+
default='pending',
|
36
|
+
db_index=True,
|
37
|
+
help_text='Domain status: "pending" (created), "ready" (audit passed), or "missing" (audit failed)'
|
38
|
+
)
|
39
|
+
|
40
|
+
receiving_enabled = models.BooleanField(
|
41
|
+
default=False,
|
42
|
+
help_text="When true, domain-level catch-all receiving is enabled via SES receipt rules"
|
43
|
+
)
|
44
|
+
|
45
|
+
s3_inbound_bucket = models.CharField(
|
46
|
+
max_length=255,
|
47
|
+
null=True,
|
48
|
+
blank=True,
|
49
|
+
help_text="S3 bucket for inbound emails (required if receiving_enabled)"
|
50
|
+
)
|
51
|
+
s3_inbound_prefix = models.CharField(
|
52
|
+
max_length=255,
|
53
|
+
default='',
|
54
|
+
blank=True,
|
55
|
+
help_text="S3 prefix for inbound emails (e.g., inbound/example.com/)"
|
56
|
+
)
|
57
|
+
|
58
|
+
dns_mode = models.CharField(
|
59
|
+
max_length=32,
|
60
|
+
default='manual',
|
61
|
+
help_text="DNS automation mode: manual | route53 | godaddy"
|
62
|
+
)
|
63
|
+
|
64
|
+
sns_topic_bounce_arn = models.CharField(
|
65
|
+
max_length=512,
|
66
|
+
null=True,
|
67
|
+
blank=True,
|
68
|
+
help_text="SNS topic ARN for SES bounce notifications"
|
69
|
+
)
|
70
|
+
sns_topic_complaint_arn = models.CharField(
|
71
|
+
max_length=512,
|
72
|
+
null=True,
|
73
|
+
blank=True,
|
74
|
+
help_text="SNS topic ARN for SES complaint notifications"
|
75
|
+
)
|
76
|
+
sns_topic_delivery_arn = models.CharField(
|
77
|
+
max_length=512,
|
78
|
+
null=True,
|
79
|
+
blank=True,
|
80
|
+
help_text="SNS topic ARN for SES delivery notifications"
|
81
|
+
)
|
82
|
+
sns_topic_inbound_arn = models.CharField(
|
83
|
+
max_length=512,
|
84
|
+
null=True,
|
85
|
+
blank=True,
|
86
|
+
help_text="SNS topic ARN for SES inbound notifications"
|
87
|
+
)
|
88
|
+
|
89
|
+
metadata = models.JSONField(default=dict, blank=True)
|
90
|
+
|
91
|
+
# Computed readiness flags updated by audit runs
|
92
|
+
can_send = models.BooleanField(
|
93
|
+
default=False,
|
94
|
+
help_text="True if outbound sending is ready per last audit"
|
95
|
+
)
|
96
|
+
can_recv = models.BooleanField(
|
97
|
+
default=False,
|
98
|
+
help_text="True if inbound receiving is ready per last audit"
|
99
|
+
)
|
100
|
+
|
101
|
+
class Meta:
|
102
|
+
db_table = "aws_email_domain"
|
103
|
+
indexes = [
|
104
|
+
models.Index(fields=["status"]),
|
105
|
+
models.Index(fields=["modified"]),
|
106
|
+
]
|
107
|
+
ordering = ["name"]
|
108
|
+
|
109
|
+
class RestMeta:
|
110
|
+
VIEW_PERMS = ["manage_aws"]
|
111
|
+
SAVE_PERMS = ["manage_aws"]
|
112
|
+
DELETE_PERMS = ["manage_aws"]
|
113
|
+
SEARCH_FIELDS = ["name", "region", "status"]
|
114
|
+
GRAPHS = {
|
115
|
+
"basic": {
|
116
|
+
"fields": [
|
117
|
+
"id",
|
118
|
+
"name",
|
119
|
+
"region",
|
120
|
+
"status",
|
121
|
+
"receiving_enabled",
|
122
|
+
]
|
123
|
+
},
|
124
|
+
"default": {
|
125
|
+
"fields": [
|
126
|
+
"id",
|
127
|
+
"name",
|
128
|
+
"region",
|
129
|
+
"status",
|
130
|
+
"receiving_enabled",
|
131
|
+
"s3_inbound_bucket",
|
132
|
+
"s3_inbound_prefix",
|
133
|
+
"dns_mode",
|
134
|
+
"sns_topic_bounce_arn",
|
135
|
+
"sns_topic_complaint_arn",
|
136
|
+
"sns_topic_delivery_arn",
|
137
|
+
"sns_topic_inbound_arn",
|
138
|
+
"metadata",
|
139
|
+
"created",
|
140
|
+
"modified",
|
141
|
+
],
|
142
|
+
"extra": [
|
143
|
+
"aws_key",
|
144
|
+
"aws_secret_masked"
|
145
|
+
]
|
146
|
+
},
|
147
|
+
}
|
148
|
+
|
149
|
+
@property
|
150
|
+
def aws_key(self):
|
151
|
+
return self.get_secret('aws_key')
|
152
|
+
|
153
|
+
@property
|
154
|
+
def aws_secret(self):
|
155
|
+
return self.get_secret('aws_secret')
|
156
|
+
|
157
|
+
@property
|
158
|
+
def aws_secret_masked(self):
|
159
|
+
secret = self.get_secret('aws_secret', '')
|
160
|
+
if len(secret) > 4:
|
161
|
+
return '*' * (len(secret) - 4) + secret[-4:]
|
162
|
+
return secret
|
163
|
+
|
164
|
+
|
165
|
+
@property
|
166
|
+
def aws_region(self):
|
167
|
+
return self.region or getattr(settings, 'AWS_REGION', 'us-east-1')
|
168
|
+
|
169
|
+
@property
|
170
|
+
def is_verified(self):
|
171
|
+
return self.status in ["verified", "ready"]
|
172
|
+
|
173
|
+
def set_aws_key(self, key):
|
174
|
+
self.set_secret('aws_key', key)
|
175
|
+
|
176
|
+
def set_aws_secret(self, secret):
|
177
|
+
self.set_secret('aws_secret', secret)
|
178
|
+
|
179
|
+
def on_rest_created(self):
|
180
|
+
"""
|
181
|
+
Automatically audit and reconcile SES/SNS configuration after this domain is created.
|
182
|
+
This keeps AWS-side resources aligned without requiring a separate call.
|
183
|
+
"""
|
184
|
+
try:
|
185
|
+
region = self.aws_region
|
186
|
+
# Audit current state (best-effort; ignore failures)
|
187
|
+
try:
|
188
|
+
desired_receiving = None
|
189
|
+
if self.receiving_enabled and self.s3_inbound_bucket:
|
190
|
+
desired_receiving = {
|
191
|
+
'bucket': self.s3_inbound_bucket,
|
192
|
+
'prefix': self.s3_inbound_prefix or '',
|
193
|
+
'rule_set': 'mojo-default-receiving',
|
194
|
+
'rule_name': f'mojo-{self.name}-catchall',
|
195
|
+
}
|
196
|
+
audit_domain_config(
|
197
|
+
domain=self.name,
|
198
|
+
region=region,
|
199
|
+
desired_receiving=desired_receiving,
|
200
|
+
)
|
201
|
+
except Exception:
|
202
|
+
# Non-fatal: continue with reconcile
|
203
|
+
pass
|
204
|
+
|
205
|
+
# Reconcile (idempotent): ensure topics/mappings and catch-all receipt rule if enabled
|
206
|
+
reconcile_domain_config(
|
207
|
+
domain=self.name,
|
208
|
+
region=region,
|
209
|
+
receiving_enabled=self.receiving_enabled,
|
210
|
+
s3_bucket=self.s3_inbound_bucket,
|
211
|
+
s3_prefix=self.s3_inbound_prefix or '',
|
212
|
+
)
|
213
|
+
except Exception:
|
214
|
+
# Swallow exceptions to avoid failing the create call; details can be inspected via /audit
|
215
|
+
pass
|
216
|
+
|
217
|
+
def __str__(self) -> str:
|
218
|
+
return self.name
|
@@ -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}"
|