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.
Files changed (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /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}"