django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.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 +279 -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.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.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,175 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+
4
+
5
+ class SentMessage(models.Model, MojoModel):
6
+ """
7
+ SentMessage
8
+
9
+ Represents an outbound email sent via AWS SES using a specific Mailbox (email address).
10
+ Tracks SES MessageId and delivery lifecycle (delivery, bounce, complaint) updated via SNS webhooks.
11
+
12
+ Notes:
13
+ - `mailbox` identifies the sending address; sending is only allowed when mailbox.allow_outbound is True.
14
+ - `ses_message_id` is populated after a successful SES send API call.
15
+ - `to_addresses`, `cc_addresses`, `bcc_addresses` are stored as JSON arrays.
16
+ - `template_name` and `template_context` support simple templated sending (EmailTemplate model can be added later).
17
+ - `status` reflects the current delivery state; `status_reason` stores detailed info (bounce/complaint payloads, errors).
18
+ """
19
+
20
+ STATUS_QUEUED = "queued"
21
+ STATUS_SENDING = "sending"
22
+ STATUS_DELIVERED = "delivered"
23
+ STATUS_BOUNCED = "bounced"
24
+ STATUS_COMPLAINED = "complained"
25
+ STATUS_FAILED = "failed"
26
+ STATUS_UNKNOWN = "unknown"
27
+
28
+ STATUS_CHOICES = [
29
+ (STATUS_QUEUED, "Queued"),
30
+ (STATUS_SENDING, "Sending"),
31
+ (STATUS_DELIVERED, "Delivered"),
32
+ (STATUS_BOUNCED, "Bounced"),
33
+ (STATUS_COMPLAINED, "Complained"),
34
+ (STATUS_FAILED, "Failed"),
35
+ (STATUS_UNKNOWN, "Unknown"),
36
+ ]
37
+
38
+ created = models.DateTimeField(auto_now_add=True, editable=False)
39
+ modified = models.DateTimeField(auto_now=True, db_index=True)
40
+
41
+ mailbox = models.ForeignKey(
42
+ "aws.Mailbox",
43
+ related_name="sent_messages",
44
+ on_delete=models.CASCADE,
45
+ help_text="Mailbox used as the sender (envelope MAIL FROM = mailbox.email)"
46
+ )
47
+
48
+ ses_message_id = models.CharField(
49
+ max_length=255,
50
+ null=True,
51
+ blank=True,
52
+ db_index=True,
53
+ help_text="AWS SES MessageId returned after a successful send"
54
+ )
55
+
56
+ # Recipients
57
+ to_addresses = models.JSONField(
58
+ default=list,
59
+ blank=True,
60
+ help_text="List of recipient addresses (To)"
61
+ )
62
+ cc_addresses = models.JSONField(
63
+ default=list,
64
+ blank=True,
65
+ help_text="List of recipient addresses (Cc)"
66
+ )
67
+ bcc_addresses = models.JSONField(
68
+ default=list,
69
+ blank=True,
70
+ help_text="List of recipient addresses (Bcc)"
71
+ )
72
+
73
+ # Content
74
+ subject = models.CharField(
75
+ max_length=512,
76
+ null=True,
77
+ blank=True,
78
+ help_text="Email subject"
79
+ )
80
+ body_text = models.TextField(
81
+ null=True,
82
+ blank=True,
83
+ help_text="Plain text body"
84
+ )
85
+ body_html = models.TextField(
86
+ null=True,
87
+ blank=True,
88
+ help_text="HTML body"
89
+ )
90
+
91
+ # Template support (simple; FK can be added later)
92
+ template_name = models.CharField(
93
+ max_length=255,
94
+ null=True,
95
+ blank=True,
96
+ help_text="Optional EmailTemplate name used to render this message"
97
+ )
98
+ template_context = models.JSONField(
99
+ default=dict,
100
+ blank=True,
101
+ help_text="Context used when rendering a template"
102
+ )
103
+
104
+ # Delivery status
105
+ status = models.CharField(
106
+ max_length=32,
107
+ choices=STATUS_CHOICES,
108
+ default=STATUS_QUEUED,
109
+ db_index=True,
110
+ help_text="Current delivery status"
111
+ )
112
+ status_reason = models.TextField(
113
+ null=True,
114
+ blank=True,
115
+ help_text="Details or raw payload for bounces/complaints/errors"
116
+ )
117
+
118
+ metadata = models.JSONField(
119
+ default=dict,
120
+ blank=True,
121
+ help_text="Arbitrary metadata for downstream processing/auditing"
122
+ )
123
+
124
+ class Meta:
125
+ db_table = "aws_sent_message"
126
+ indexes = [
127
+ models.Index(fields=["modified"]),
128
+ models.Index(fields=["status"]),
129
+ models.Index(fields=["ses_message_id"]),
130
+ ]
131
+ ordering = ["-created", "id"]
132
+
133
+ class RestMeta:
134
+ VIEW_PERMS = ["manage_aws"]
135
+ SAVE_PERMS = ["manage_aws"]
136
+ DELETE_PERMS = ["manage_aws"]
137
+ SEARCH_FIELDS = ["subject", "ses_message_id"]
138
+ GRAPHS = {
139
+ "basic": {
140
+ "fields": [
141
+ "id",
142
+ "mailbox",
143
+ "ses_message_id",
144
+ "subject",
145
+ "to_addresses",
146
+ "status",
147
+ "created",
148
+ ],
149
+ "graphs": {"mailbox": "basic"}
150
+ },
151
+ "default": {
152
+ "fields": [
153
+ "id",
154
+ "mailbox",
155
+ "ses_message_id",
156
+ "to_addresses",
157
+ "cc_addresses",
158
+ "bcc_addresses",
159
+ "subject",
160
+ "body_text",
161
+ "body_html",
162
+ "template_name",
163
+ "template_context",
164
+ "status",
165
+ "status_reason",
166
+ "metadata",
167
+ "created",
168
+ "modified",
169
+ ],
170
+ "graphs": {"mailbox": "basic"}
171
+ },
172
+ }
173
+
174
+ def __str__(self) -> str:
175
+ return self.subject or self.ses_message_id or f"SentMessage {self.pk}"
@@ -1 +1,7 @@
1
1
  from .s3 import *
2
+ from .email import *
3
+ from .email_ops import *
4
+ from .messages import *
5
+ from .sns import *
6
+ from .send import *
7
+ from .templates import *
@@ -0,0 +1,33 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps.aws.models import EmailDomain, Mailbox
3
+
4
+
5
+ """
6
+ AWS Email REST Handlers
7
+
8
+ Endpoints:
9
+ - Domain CRUD:
10
+ - GET/POST/PUT/DELETE /aws/email/domain
11
+ - GET/POST/PUT/DELETE /aws/email/domain/<int:pk>
12
+
13
+ - Mailbox CRUD:
14
+ - GET/POST/PUT/DELETE /aws/email/mailbox
15
+ - GET/POST/PUT/DELETE /aws/email/mailbox/<int:pk>
16
+
17
+ These handlers delegate to the models' on_rest_request, which uses RestMeta for
18
+ permission checks, graphs, and default CRUD behavior.
19
+ """
20
+
21
+
22
+ @md.URL('email/domain')
23
+ @md.URL('email/domain/<int:pk>')
24
+ @md.requires_perms("manage_aws")
25
+ def on_email_domain(request, pk=None):
26
+ return EmailDomain.on_rest_request(request, pk)
27
+
28
+
29
+ @md.URL('email/mailbox')
30
+ @md.URL('email/mailbox/<int:pk>')
31
+ @md.requires_perms("manage_aws")
32
+ def on_mailbox(request, pk=None):
33
+ return Mailbox.on_rest_request(request, pk)
@@ -0,0 +1,183 @@
1
+ from typing import Dict, Any
2
+
3
+ from mojo import decorators as md
4
+ from mojo import JsonResponse
5
+ from mojo.helpers import logit
6
+
7
+ # Use the new email_ops service
8
+ from mojo.apps.aws.services.email_ops import (
9
+ onboard_email_domain,
10
+ audit_email_domain,
11
+ reconcile_email_domain,
12
+ generate_audit_recommendations,
13
+ EmailDomainNotFound,
14
+ InvalidConfiguration,
15
+ )
16
+
17
+ logger = logit.get_logger("email", "email.log")
18
+
19
+
20
+ def _get_json(request) -> Dict[str, Any]:
21
+ return getattr(request, "DATA", {}) or {}
22
+
23
+
24
+ @md.URL("email/domain/<int:pk>/onboard")
25
+ @md.requires_perms("manage_aws")
26
+ def on_email_domain_onboard(request, pk: int):
27
+ """
28
+ Kick off domain onboarding:
29
+ - Request SES domain verification + DKIM tokens
30
+ - Compute required DNS records (manual or automated via GoDaddy if requested)
31
+ - Ensure SNS topics + notification mappings
32
+ - Optionally enable receiving (catch-all → S3 + SNS)
33
+ - Optionally enable MAIL FROM (returns DNS to add)
34
+ """
35
+ if request.method != "POST":
36
+ return JsonResponse({"error": "Method not allowed"}, status=405)
37
+
38
+ payload = _get_json(request)
39
+
40
+ try:
41
+ result = onboard_email_domain(
42
+ domain_pk=pk,
43
+ region=payload.get("region"),
44
+ receiving_enabled=payload.get("receiving_enabled"),
45
+ s3_bucket=payload.get("s3_inbound_bucket"),
46
+ s3_prefix=payload.get("s3_inbound_prefix"),
47
+ ensure_mail_from=bool(payload.get("ensure_mail_from", False)),
48
+ mail_from_subdomain=payload.get("mail_from_subdomain", "feedback"),
49
+ dns_mode=payload.get("dns_mode"),
50
+ endpoints=payload.get("endpoints") or {
51
+ "bounce": payload.get("bounce_endpoint"),
52
+ "complaint": payload.get("complaint_endpoint"),
53
+ "delivery": payload.get("delivery_endpoint"),
54
+ "inbound": payload.get("inbound_endpoint"),
55
+ },
56
+ access_key=payload.get("aws_access_key"),
57
+ secret_key=payload.get("aws_secret_key"),
58
+ godaddy_key=payload.get("godaddy_key"),
59
+ godaddy_secret=payload.get("godaddy_secret"),
60
+ )
61
+
62
+ return JsonResponse({
63
+ "status": True,
64
+ "data": {
65
+ "domain": result.domain,
66
+ "region": result.region,
67
+ "dns_records": result.dns_records,
68
+ "dkim_tokens": result.dkim_tokens,
69
+ "topic_arns": result.topic_arns,
70
+ "receipt_rule": result.receipt_rule,
71
+ "rule_set": result.rule_set,
72
+ "notes": result.notes,
73
+ }
74
+ })
75
+ except EmailDomainNotFound:
76
+ return JsonResponse({"error": "EmailDomain not found", "code": 404}, status=404)
77
+ except InvalidConfiguration as e:
78
+ return JsonResponse({"error": str(e)}, status=400)
79
+ except Exception as e:
80
+ logger.error(f"onboard error for domain pk={pk}: {e}")
81
+ return JsonResponse({"error": str(e)}, status=500)
82
+
83
+
84
+ @md.URL("email/domain/<int:pk>/audit")
85
+ @md.requires_perms("manage_aws")
86
+ def on_email_domain_audit(request, pk: int):
87
+ """
88
+ Audit SES/SNS/S3 configuration for the domain and return a drift report.
89
+ Uses the model configuration to compute desired receiving.
90
+ """
91
+ if request.method not in ("GET", "POST"):
92
+ return JsonResponse({"error": "Method not allowed"}, status=405)
93
+
94
+ payload = _get_json(request) if request.method == "POST" else {}
95
+
96
+ try:
97
+ result = audit_email_domain(
98
+ domain_pk=pk,
99
+ region=payload.get("region"),
100
+ access_key=payload.get("aws_access_key"),
101
+ secret_key=payload.get("aws_secret_key"),
102
+ rule_set=payload.get("rule_set"),
103
+ rule_name=payload.get("rule_name"),
104
+ )
105
+
106
+ return JsonResponse({
107
+ "status": True,
108
+ "data": {
109
+ "domain": result.domain,
110
+ "region": result.region,
111
+ "status": result.status,
112
+ "audit_pass": result.audit_pass,
113
+ "checks": result.checks,
114
+ "items": [
115
+ {
116
+ "resource": it.resource,
117
+ "desired": it.desired,
118
+ "current": it.current,
119
+ "status": it.status
120
+ } for it in result.items
121
+ ],
122
+ "recommendations": generate_audit_recommendations(result.report)
123
+ }
124
+ })
125
+ except EmailDomainNotFound:
126
+ return JsonResponse({"error": "EmailDomain not found", "code": 404}, status=404)
127
+ except Exception as e:
128
+ logger.error(f"audit error for domain pk={pk}: {e}")
129
+ return JsonResponse({"error": str(e)}, status=500)
130
+
131
+
132
+ @md.URL("email/domain/<int:pk>/reconcile")
133
+ @md.requires_perms("manage_aws")
134
+ def on_email_domain_reconcile(request, pk: int):
135
+ """
136
+ Attempt to reconcile SES/SNS for the domain:
137
+ - Ensure SNS topics and notification mappings
138
+ - Ensure receiving catch-all rule (if receiving_enabled)
139
+ - Optionally configure MAIL FROM
140
+ Does not modify DNS; use onboarding + DNS mode or apply manually.
141
+ """
142
+ if request.method != "POST":
143
+ return JsonResponse({"error": "Method not allowed"}, status=405)
144
+
145
+ payload = _get_json(request)
146
+
147
+ try:
148
+ result = reconcile_email_domain(
149
+ domain_pk=pk,
150
+ region=payload.get("region"),
151
+ receiving_enabled=payload.get("receiving_enabled"),
152
+ s3_bucket=payload.get("s3_inbound_bucket"),
153
+ s3_prefix=payload.get("s3_inbound_prefix"),
154
+ ensure_mail_from=bool(payload.get("ensure_mail_from", False)),
155
+ mail_from_subdomain=payload.get("mail_from_subdomain", "feedback"),
156
+ endpoints=payload.get("endpoints") or {
157
+ "bounce": payload.get("bounce_endpoint"),
158
+ "complaint": payload.get("complaint_endpoint"),
159
+ "delivery": payload.get("delivery_endpoint"),
160
+ "inbound": payload.get("inbound_endpoint"),
161
+ },
162
+ access_key=payload.get("aws_access_key"),
163
+ secret_key=payload.get("aws_secret_key"),
164
+ )
165
+
166
+ return JsonResponse({
167
+ "status": True,
168
+ "data": {
169
+ "domain": result.domain,
170
+ "region": result.region,
171
+ "topic_arns": result.topic_arns,
172
+ "receipt_rule": result.receipt_rule,
173
+ "rule_set": result.rule_set,
174
+ "notes": result.notes,
175
+ }
176
+ })
177
+ except EmailDomainNotFound:
178
+ return JsonResponse({"error": "EmailDomain not found", "code": 404}, status=404)
179
+ except InvalidConfiguration as e:
180
+ return JsonResponse({"error": str(e)}, status=400)
181
+ except Exception as e:
182
+ logger.error(f"reconcile error for domain pk={pk}: {e}")
183
+ return JsonResponse({"error": str(e)}, status=500)
@@ -0,0 +1,32 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps.aws.models import IncomingEmail, SentMessage
3
+
4
+ """
5
+ AWS Email Messages REST Handlers
6
+
7
+ Endpoints:
8
+ - Incoming emails (list/detail via model.on_rest_request):
9
+ - GET/POST/PUT/DELETE /email/incoming
10
+ - GET/POST/PUT/DELETE /email/incoming/<int:pk>
11
+
12
+ - Sent messages (list/detail via model.on_rest_request):
13
+ - GET/POST/PUT/DELETE /email/sent
14
+ - GET/POST/PUT/DELETE /email/sent/<int:pk>
15
+
16
+ All endpoints require the "manage_aws" permission and delegate to the models' on_rest_request
17
+ for CRUD operations, leveraging RestMeta permissions and graphs.
18
+ """
19
+
20
+
21
+ @md.URL('email/incoming')
22
+ @md.URL('email/incoming/<int:pk>')
23
+ @md.requires_perms("manage_aws")
24
+ def on_incoming_email(request, pk=None):
25
+ return IncomingEmail.on_rest_request(request, pk)
26
+
27
+
28
+ @md.URL('email/sent')
29
+ @md.URL('email/sent/<int:pk>')
30
+ @md.requires_perms("manage_aws")
31
+ def on_sent_message(request, pk=None):
32
+ return SentMessage.on_rest_request(request, pk)
@@ -0,0 +1,101 @@
1
+ from mojo.apps.aws.services import send_template_email
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from mojo import decorators as md
5
+ from mojo import JsonResponse
6
+ from mojo.apps.aws.models import Mailbox, SentMessage, EmailDomain, EmailTemplate
7
+ from mojo.helpers.aws.ses import EmailSender
8
+ from mojo.helpers.settings import settings
9
+ from mojo.helpers import logit
10
+
11
+ logger = logit.get_logger("email", "email.log")
12
+
13
+
14
+ def _as_list(value: Any) -> List[str]:
15
+ if value is None:
16
+ return []
17
+ if isinstance(value, (list, tuple)):
18
+ return [str(v).strip() for v in value if str(v).strip()]
19
+ return [str(value).strip()] if str(value).strip() else []
20
+
21
+
22
+ @md.URL("email/send")
23
+ @md.requires_perms("manage_aws")
24
+ def on_send_email(request):
25
+ """
26
+ Send an email through AWS SES using a Mailbox resolved by from_email.
27
+
28
+ Request (POST JSON):
29
+ {
30
+ "from_email": "support@example.com", // required, resolves Mailbox
31
+ "to": ["user@example.org"], // required (list or string)
32
+ "cc": [], // optional
33
+ "bcc": [], // optional
34
+ "subject": "Hello", // required if not using template_name
35
+ "body_text": "Text body", // optional
36
+ "body_html": "<p>HTML body</p>", // optional
37
+ "reply_to": ["replies@example.com"], // optional
38
+ "template_name": "db-template-optional", // optional, uses DB EmailTemplate if provided
39
+ "ses_template_name": "ses-template-optional", // optional, uses AWS SES managed template
40
+ "template_context": { ... }, // optional, for DB/SES template context
41
+ "aws_access_key": "...", // optional, defaults to settings
42
+ "aws_secret_key": "...", // optional, defaults to settings
43
+ "allow_unverified": false // optional, allow send even if domain.status != 'verified'
44
+ }
45
+
46
+ Behavior:
47
+ - Resolves the Mailbox by from_email (case-insensitive).
48
+ - Ensures mailbox.allow_outbound is True.
49
+ - Uses mailbox.domain.region (or settings.AWS_REGION) to send via SES.
50
+ - If template_name is provided and matches a DB EmailTemplate, renders and uses EmailSender.send_email with the rendered subject/body.
51
+ If ses_template_name is provided, uses EmailSender.send_template_email (AWS SES managed template).
52
+ Otherwise uses EmailSender.send_email with subject/body_text/body_html.
53
+ - Creates a SentMessage row and updates with SES MessageId and status.
54
+ """
55
+ if request.method != "POST":
56
+ return JsonResponse({"error": "Method not allowed"}, status=405)
57
+
58
+ data: Dict[str, Any] = getattr(request, "DATA", {}) or {}
59
+
60
+ from_email = (data.get("from_email") or "").strip()
61
+ if not from_email:
62
+ return JsonResponse({"error": "from_email is required"}, status=400)
63
+
64
+ # Resolve Mailbox by email (case-insensitive)
65
+ mailbox = Mailbox.objects.select_related("domain").filter(email__iexact=from_email).first()
66
+ if not mailbox:
67
+ return JsonResponse({"error": f"Mailbox not found for from_email={from_email}", "code": 404}, status=404)
68
+
69
+ if not mailbox.allow_outbound:
70
+ return JsonResponse({"error": "Outbound sending is disabled for this mailbox", "code": 403}, status=403)
71
+
72
+
73
+ to = _as_list(data.get("to"))
74
+ cc = _as_list(data.get("cc"))
75
+ bcc = _as_list(data.get("bcc"))
76
+ reply_to = _as_list(data.get("reply_to")) or [from_email]
77
+
78
+ if not to:
79
+ return JsonResponse({"error": "At least one recipient in 'to' is required"}, status=400)
80
+
81
+ subject = (data.get("subject") or "").strip()
82
+ body_text = data.get("body_text")
83
+ body_html = data.get("body_html")
84
+ template_name = (data.get("template_name") or "").strip() or None
85
+ template_context = data.get("template_context") or {}
86
+
87
+ if template_name:
88
+ res = mailbox.send_template_email(
89
+ to, template_name, template_context,
90
+ cc, bcc, reply_to)
91
+ else:
92
+ res = mailbox.send_email(
93
+ to=to,
94
+ subject=subject,
95
+ body_text=body_text,
96
+ body_html=body_html,
97
+ cc=cc,
98
+ bcc=bcc,
99
+ reply_to=reply_to
100
+ )
101
+ return res.on_rest_get(request)