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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +281 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /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
+ )