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,548 @@
1
+ """
2
+ AWS SES Domain Management Service
3
+
4
+ This service provides domain onboarding, auditing, and reconciliation functionality
5
+ for AWS SES email domains. It encapsulates the complex domain setup process and
6
+ provides user-friendly recommendations for configuration issues.
7
+
8
+ Usage:
9
+ from mojo.apps.aws.services.email_ops import (
10
+ onboard_email_domain,
11
+ audit_email_domain,
12
+ reconcile_email_domain,
13
+ generate_audit_recommendations
14
+ )
15
+
16
+ # Onboard a new domain
17
+ result = onboard_email_domain(
18
+ domain_pk=1,
19
+ region="us-east-1",
20
+ receiving_enabled=True,
21
+ s3_bucket="my-emails",
22
+ dns_mode="manual"
23
+ )
24
+
25
+ # Audit domain configuration
26
+ audit = audit_email_domain(domain_pk=1, region="us-east-1")
27
+ recommendations = generate_audit_recommendations(audit.report)
28
+
29
+ # Fix configuration drift
30
+ reconcile_result = reconcile_email_domain(
31
+ domain_pk=1,
32
+ receiving_enabled=True,
33
+ s3_bucket="my-emails"
34
+ )
35
+ """
36
+
37
+ from typing import Dict, Any, Optional, List, NamedTuple
38
+ from dataclasses import dataclass
39
+
40
+ from mojo.apps.aws.models import EmailDomain
41
+ from mojo.helpers.settings import settings
42
+ from mojo.helpers import logit
43
+
44
+ # Orchestration helpers
45
+ from mojo.helpers.aws.ses_domain import (
46
+ onboard_domain,
47
+ audit_domain_config,
48
+ reconcile_domain_config,
49
+ SnsEndpoints,
50
+ DnsRecord,
51
+ apply_dns_records_godaddy,
52
+ )
53
+
54
+ logger = logit.get_logger("email", "email.log")
55
+
56
+
57
+ # Exceptions
58
+ class EmailDomainNotFound(Exception):
59
+ pass
60
+
61
+ class InvalidConfiguration(Exception):
62
+ pass
63
+
64
+
65
+ # Data structures
66
+ @dataclass
67
+ class OnboardResult:
68
+ domain: str
69
+ region: str
70
+ dns_records: List[Dict[str, Any]]
71
+ dkim_tokens: List[str]
72
+ topic_arns: Dict[str, str]
73
+ receipt_rule: Optional[str]
74
+ rule_set: Optional[str]
75
+ notes: List[str]
76
+
77
+
78
+ @dataclass
79
+ class AuditResult:
80
+ domain: str
81
+ region: str
82
+ status: str
83
+ audit_pass: bool
84
+ checks: Dict[str, bool]
85
+ items: List[Any]
86
+ report: Any # Store original report for recommendations
87
+
88
+
89
+ @dataclass
90
+ class ReconcileResult:
91
+ domain: str
92
+ region: str
93
+ topic_arns: Dict[str, str]
94
+ receipt_rule: Optional[str]
95
+ rule_set: Optional[str]
96
+ notes: List[str]
97
+
98
+
99
+ # Helper functions
100
+ def _get_domain(domain_pk: int) -> EmailDomain:
101
+ """Get EmailDomain by primary key or raise exception"""
102
+ try:
103
+ return EmailDomain.objects.get(pk=domain_pk)
104
+ except EmailDomain.DoesNotExist:
105
+ raise EmailDomainNotFound(f"EmailDomain not found with pk={domain_pk}")
106
+
107
+
108
+ def _parse_endpoints(payload: Dict[str, Any]) -> SnsEndpoints:
109
+ """Parse SNS endpoints from payload"""
110
+ ep = payload.get("endpoints") or {}
111
+ return SnsEndpoints(
112
+ bounce=ep.get("bounce") or payload.get("bounce_endpoint"),
113
+ complaint=ep.get("complaint") or payload.get("complaint_endpoint"),
114
+ delivery=ep.get("delivery") or payload.get("delivery_endpoint"),
115
+ inbound=ep.get("inbound") or payload.get("inbound_endpoint"),
116
+ )
117
+
118
+
119
+ def _dns_records_to_dict(records: List[DnsRecord]) -> List[Dict[str, Any]]:
120
+ """Convert DnsRecord objects to dictionaries"""
121
+ return [{"type": r.type, "name": r.name, "value": r.value, "ttl": r.ttl} for r in records]
122
+
123
+
124
+ def _get_aws_credentials(domain: EmailDomain,
125
+ access_key: Optional[str] = None,
126
+ secret_key: Optional[str] = None) -> tuple[Optional[str], Optional[str]]:
127
+ """Get AWS credentials with domain/settings fallback"""
128
+ return (
129
+ access_key or domain.aws_key or getattr(settings, "AWS_KEY", None),
130
+ secret_key or domain.aws_secret or getattr(settings, "AWS_SECRET", None)
131
+ )
132
+
133
+
134
+ def generate_audit_recommendations(report) -> List[Dict[str, Any]]:
135
+ """Generate user-friendly recommendations based on audit results"""
136
+ recommendations = []
137
+
138
+ for item in report.items:
139
+ resource = item.resource
140
+ status = item.status
141
+ current = str(item.current)
142
+
143
+ if status != "conflict":
144
+ continue
145
+
146
+ recommendation = {"resource": resource, "severity": "high", "action": "", "explanation": ""}
147
+
148
+ # Check for credential/permission issues
149
+ if ("AccessDenied" in current or "not authorized" in current or
150
+ "InvalidSignatureException" in current or "SignatureDoesNotMatch" in current):
151
+
152
+ if "ses-smtp-user" in current:
153
+ recommendation.update({
154
+ "severity": "critical",
155
+ "action": "Replace SMTP credentials with full AWS API credentials",
156
+ "explanation": "You're using SMTP-only credentials that can't manage SES settings. You need AWS API credentials with SES permissions to configure domains."
157
+ })
158
+ else:
159
+ recommendation.update({
160
+ "severity": "critical",
161
+ "action": "Fix AWS credentials or permissions",
162
+ "explanation": "Your AWS credentials are invalid, expired, or don't have the required SES permissions. Check your AWS access key and secret key."
163
+ })
164
+
165
+ # Resource-specific recommendations
166
+ elif resource == "ses.account.production_access":
167
+ if not item.desired.get("ProductionAccessEnabled"):
168
+ recommendation.update({
169
+ "action": "Request SES production access",
170
+ "explanation": "Your SES account is in sandbox mode. You can only send to verified email addresses. Request production access through AWS console to send to any email."
171
+ })
172
+
173
+ elif resource == "ses.identity.verification":
174
+ recommendation.update({
175
+ "action": "Verify your domain in AWS SES",
176
+ "explanation": "Add the required DNS TXT record to prove you own this domain. Check AWS SES console for the verification record."
177
+ })
178
+
179
+ elif resource == "ses.identity.dkim":
180
+ recommendation.update({
181
+ "action": "Set up DKIM for better email delivery",
182
+ "explanation": "Add DKIM DNS records to improve email authenticity and delivery rates. This helps prevent emails from being marked as spam."
183
+ })
184
+
185
+ elif resource == "ses.identity.notification_topics":
186
+ recommendation.update({
187
+ "severity": "medium",
188
+ "action": "Configure bounce and complaint handling",
189
+ "explanation": "Set up SNS topics to track bounced and complained emails. This is required for production email sending."
190
+ })
191
+
192
+ elif "s3_bucket" in resource:
193
+ recommendation.update({
194
+ "severity": "medium",
195
+ "action": "Create or configure S3 bucket for incoming emails",
196
+ "explanation": "If you want to receive emails, you need an S3 bucket where AWS will store incoming messages."
197
+ })
198
+
199
+ elif "receiving_rule" in resource:
200
+ recommendation.update({
201
+ "severity": "low",
202
+ "action": "Configure email receiving rules",
203
+ "explanation": "Set up SES rules to automatically process incoming emails and store them in S3."
204
+ })
205
+
206
+ else:
207
+ recommendation.update({
208
+ "action": "Review AWS SES configuration",
209
+ "explanation": "There's a configuration issue that needs attention. Check the AWS SES console for more details."
210
+ })
211
+
212
+ recommendations.append(recommendation)
213
+
214
+ # Add overall recommendations based on checks
215
+ checks = report.checks
216
+ if not checks.get("ses_verified") and not any(r["resource"] == "ses.identity.verification" for r in recommendations):
217
+ recommendations.insert(0, {
218
+ "resource": "domain_verification",
219
+ "severity": "critical",
220
+ "action": "Verify your domain ownership first",
221
+ "explanation": "Before you can send emails, you must prove you own this domain by adding a DNS record. This is the first step in email setup."
222
+ })
223
+
224
+ return recommendations
225
+
226
+
227
+ # Public API
228
+ def onboard_email_domain(
229
+ domain_pk: int,
230
+ *,
231
+ region: Optional[str] = None,
232
+ receiving_enabled: Optional[bool] = None,
233
+ s3_bucket: Optional[str] = None,
234
+ s3_prefix: Optional[str] = None,
235
+ ensure_mail_from: bool = False,
236
+ mail_from_subdomain: str = "feedback",
237
+ dns_mode: Optional[str] = None,
238
+ endpoints: Optional[Dict[str, str]] = None,
239
+ access_key: Optional[str] = None,
240
+ secret_key: Optional[str] = None,
241
+ godaddy_key: Optional[str] = None,
242
+ godaddy_secret: Optional[str] = None,
243
+ ) -> OnboardResult:
244
+ """
245
+ Onboard an email domain for AWS SES.
246
+
247
+ Args:
248
+ domain_pk: Primary key of the EmailDomain to onboard
249
+ region: AWS region (defaults to domain.region or settings.AWS_REGION)
250
+ receiving_enabled: Enable email receiving (defaults to domain.receiving_enabled)
251
+ s3_bucket: S3 bucket for incoming emails
252
+ s3_prefix: S3 prefix for incoming emails
253
+ ensure_mail_from: Configure MAIL FROM subdomain
254
+ mail_from_subdomain: Subdomain for MAIL FROM
255
+ dns_mode: "manual" or "godaddy" for DNS record application
256
+ endpoints: SNS endpoint configuration
257
+ access_key, secret_key: AWS credentials override
258
+ godaddy_key, godaddy_secret: GoDaddy API credentials for automatic DNS
259
+
260
+ Returns:
261
+ OnboardResult with DNS records, DKIM tokens, and configuration details
262
+
263
+ Raises:
264
+ EmailDomainNotFound: If domain_pk doesn't exist
265
+ InvalidConfiguration: If receiving_enabled but no s3_bucket
266
+ """
267
+ domain = _get_domain(domain_pk)
268
+
269
+ # Resolve configuration with defaults
270
+ region = region or domain.region or getattr(settings, "AWS_REGION", "us-east-1")
271
+ receiving_enabled = receiving_enabled if receiving_enabled is not None else domain.receiving_enabled
272
+ s3_bucket = s3_bucket or domain.s3_inbound_bucket
273
+ s3_prefix = s3_prefix or domain.s3_inbound_prefix or ""
274
+ dns_mode = dns_mode or domain.dns_mode or "manual"
275
+
276
+ if receiving_enabled and not s3_bucket:
277
+ raise InvalidConfiguration("s3_bucket is required when receiving_enabled is true")
278
+
279
+ # Get AWS credentials
280
+ access_key_final, secret_key_final = _get_aws_credentials(domain, access_key, secret_key)
281
+
282
+ # Parse endpoints
283
+ sns_endpoints = _parse_endpoints(endpoints or {})
284
+
285
+ try:
286
+ result = onboard_domain(
287
+ domain=domain.name,
288
+ region=region,
289
+ access_key=access_key_final,
290
+ secret_key=secret_key_final,
291
+ receiving_enabled=receiving_enabled,
292
+ s3_bucket=s3_bucket,
293
+ s3_prefix=s3_prefix,
294
+ dns_mode=dns_mode,
295
+ ensure_mail_from=ensure_mail_from,
296
+ mail_from_subdomain=mail_from_subdomain,
297
+ endpoints=sns_endpoints,
298
+ )
299
+
300
+ # Apply DNS via GoDaddy if requested
301
+ if dns_mode == "godaddy" and godaddy_key and godaddy_secret:
302
+ apply_dns_records_godaddy(
303
+ domain=domain.name,
304
+ records=result.dns_records,
305
+ api_key=godaddy_key,
306
+ api_secret=godaddy_secret,
307
+ )
308
+ result.notes.append("Applied DNS via GoDaddy")
309
+ elif dns_mode == "godaddy":
310
+ result.notes.append("DNS mode is GoDaddy but credentials not provided; returning records for manual apply")
311
+
312
+ # Update domain configuration
313
+ updates = {}
314
+ if domain.region != region:
315
+ updates["region"] = region
316
+ if domain.receiving_enabled != receiving_enabled:
317
+ updates["receiving_enabled"] = receiving_enabled
318
+ if s3_bucket and domain.s3_inbound_bucket != s3_bucket:
319
+ updates["s3_inbound_bucket"] = s3_bucket
320
+ if (s3_prefix or "") != (domain.s3_inbound_prefix or ""):
321
+ updates["s3_inbound_prefix"] = s3_prefix
322
+ if dns_mode and domain.dns_mode != dns_mode:
323
+ updates["dns_mode"] = dns_mode
324
+
325
+ if updates:
326
+ for k, v in updates.items():
327
+ setattr(domain, k, v)
328
+ domain.save(update_fields=list(updates.keys()) + ["modified"])
329
+
330
+ return OnboardResult(
331
+ domain=result.domain,
332
+ region=result.region,
333
+ dns_records=_dns_records_to_dict(result.dns_records),
334
+ dkim_tokens=result.dkim_tokens,
335
+ topic_arns=result.topic_arns,
336
+ receipt_rule=result.receipt_rule,
337
+ rule_set=result.rule_set,
338
+ notes=result.notes,
339
+ )
340
+
341
+ except Exception as e:
342
+ logger.error(f"onboard error for domain {domain.name}: {e}")
343
+ raise
344
+
345
+
346
+ def audit_email_domain(
347
+ domain_pk: int,
348
+ *,
349
+ region: Optional[str] = None,
350
+ access_key: Optional[str] = None,
351
+ secret_key: Optional[str] = None,
352
+ rule_set: Optional[str] = None,
353
+ rule_name: Optional[str] = None,
354
+ ) -> AuditResult:
355
+ """
356
+ Audit AWS SES configuration for an email domain.
357
+
358
+ Args:
359
+ domain_pk: Primary key of the EmailDomain to audit
360
+ region: AWS region override
361
+ access_key, secret_key: AWS credentials override
362
+ rule_set: SES receiving rule set name
363
+ rule_name: SES receiving rule name
364
+
365
+ Returns:
366
+ AuditResult with configuration status and drift analysis
367
+
368
+ Raises:
369
+ EmailDomainNotFound: If domain_pk doesn't exist
370
+ """
371
+ domain = _get_domain(domain_pk)
372
+
373
+ region = region or domain.region or getattr(settings, "AWS_REGION", "us-east-1")
374
+ access_key_final, secret_key_final = _get_aws_credentials(domain, access_key, secret_key)
375
+
376
+ # Configure desired receiving if enabled
377
+ desired_receiving = None
378
+ if domain.receiving_enabled and domain.s3_inbound_bucket:
379
+ desired_receiving = {
380
+ "bucket": domain.s3_inbound_bucket,
381
+ "prefix": domain.s3_inbound_prefix or "",
382
+ "rule_set": rule_set or "mojo-default-receiving",
383
+ "rule_name": rule_name or f"mojo-{domain.name}-catchall",
384
+ "inbound_topic_arn": getattr(domain, "sns_topic_inbound_arn", None),
385
+ }
386
+
387
+ try:
388
+ report = audit_domain_config(
389
+ domain=domain.name,
390
+ region=region,
391
+ access_key=access_key_final,
392
+ secret_key=secret_key_final,
393
+ desired_receiving=desired_receiving,
394
+ desired_topics={
395
+ "bounce": getattr(domain, "sns_topic_bounce_arn", None),
396
+ "complaint": getattr(domain, "sns_topic_complaint_arn", None),
397
+ "delivery": getattr(domain, "sns_topic_delivery_arn", None),
398
+ },
399
+ )
400
+
401
+ # Update domain status based on audit results
402
+ can_send = bool(
403
+ report.checks.get("ses_verified") and
404
+ report.checks.get("dkim_verified") and
405
+ report.checks.get("ses_production_access") and
406
+ report.checks.get("notification_topics_ok")
407
+ )
408
+
409
+ can_recv = False
410
+ if domain.receiving_enabled:
411
+ can_recv = bool(
412
+ report.checks.get("s3_bucket_exists") and
413
+ report.checks.get("receiving_rule_s3_ok") and
414
+ report.checks.get("receiving_rule_sns_ok") and
415
+ report.checks.get("sns_topics_exist") and
416
+ report.checks.get("sns_subscriptions_confirmed")
417
+ )
418
+
419
+ # Determine status: "verified" if SES domain is verified, "ready" if fully configured, else "missing"
420
+ if report.checks.get("ses_verified"):
421
+ new_status = "verified"
422
+ if report.audit_pass:
423
+ new_status = "ready"
424
+ else:
425
+ new_status = "missing"
426
+
427
+ # Track what we're updating for debugging
428
+ updates = {}
429
+ if domain.status != new_status:
430
+ updates["status"] = new_status
431
+ if domain.can_send != can_send:
432
+ updates["can_send"] = can_send
433
+ if domain.can_recv != can_recv:
434
+ updates["can_recv"] = can_recv
435
+
436
+ if updates:
437
+ logger.info(f"Updating domain {domain.name} (pk={domain_pk}) with changes: {updates}")
438
+ for k, v in updates.items():
439
+ setattr(domain, k, v)
440
+ domain.save(update_fields=list(updates.keys()) + ["modified"])
441
+ logger.info(f"Successfully updated domain {domain.name} status to '{new_status}'")
442
+ else:
443
+ logger.info(f"Domain {domain.name} status unchanged: {domain.status}")
444
+
445
+ return AuditResult(
446
+ domain=report.domain,
447
+ region=report.region,
448
+ status=report.status,
449
+ audit_pass=report.audit_pass,
450
+ checks=report.checks,
451
+ items=report.items,
452
+ report=report # Store original for recommendations
453
+ )
454
+
455
+ except Exception as e:
456
+ logger.error(f"Audit error for domain {domain.name}: {e}")
457
+ raise
458
+
459
+
460
+ def reconcile_email_domain(
461
+ domain_pk: int,
462
+ *,
463
+ region: Optional[str] = None,
464
+ receiving_enabled: Optional[bool] = None,
465
+ s3_bucket: Optional[str] = None,
466
+ s3_prefix: Optional[str] = None,
467
+ ensure_mail_from: bool = False,
468
+ mail_from_subdomain: str = "feedback",
469
+ endpoints: Optional[Dict[str, str]] = None,
470
+ access_key: Optional[str] = None,
471
+ secret_key: Optional[str] = None,
472
+ ) -> ReconcileResult:
473
+ """
474
+ Reconcile AWS SES/SNS configuration for an email domain.
475
+
476
+ Args:
477
+ domain_pk: Primary key of the EmailDomain to reconcile
478
+ region: AWS region override
479
+ receiving_enabled: Enable email receiving
480
+ s3_bucket: S3 bucket for incoming emails
481
+ s3_prefix: S3 prefix for incoming emails
482
+ ensure_mail_from: Configure MAIL FROM subdomain
483
+ mail_from_subdomain: Subdomain for MAIL FROM
484
+ endpoints: SNS endpoint configuration
485
+ access_key, secret_key: AWS credentials override
486
+
487
+ Returns:
488
+ ReconcileResult with applied configuration changes
489
+
490
+ Raises:
491
+ EmailDomainNotFound: If domain_pk doesn't exist
492
+ InvalidConfiguration: If receiving_enabled but no s3_bucket
493
+ """
494
+ domain = _get_domain(domain_pk)
495
+
496
+ region = region or domain.region or getattr(settings, "AWS_REGION", "us-east-1")
497
+ receiving_enabled = receiving_enabled if receiving_enabled is not None else domain.receiving_enabled
498
+ s3_bucket = s3_bucket or domain.s3_inbound_bucket
499
+ s3_prefix = s3_prefix or domain.s3_inbound_prefix or ""
500
+
501
+ if receiving_enabled and not s3_bucket:
502
+ raise InvalidConfiguration("s3_bucket is required when receiving_enabled is true")
503
+
504
+ access_key_final, secret_key_final = _get_aws_credentials(domain, access_key, secret_key)
505
+ sns_endpoints = _parse_endpoints(endpoints or {})
506
+
507
+ try:
508
+ result = reconcile_domain_config(
509
+ domain=domain.name,
510
+ region=region,
511
+ receiving_enabled=receiving_enabled,
512
+ s3_bucket=s3_bucket,
513
+ s3_prefix=s3_prefix,
514
+ endpoints=sns_endpoints,
515
+ access_key=access_key_final,
516
+ secret_key=secret_key_final,
517
+ ensure_mail_from=ensure_mail_from,
518
+ mail_from_subdomain=mail_from_subdomain,
519
+ )
520
+
521
+ # Update domain configuration
522
+ updates = {}
523
+ if domain.region != region:
524
+ updates["region"] = region
525
+ if domain.receiving_enabled != receiving_enabled:
526
+ updates["receiving_enabled"] = receiving_enabled
527
+ if s3_bucket and domain.s3_inbound_bucket != s3_bucket:
528
+ updates["s3_inbound_bucket"] = s3_bucket
529
+ if (s3_prefix or "") != (domain.s3_inbound_prefix or ""):
530
+ updates["s3_inbound_prefix"] = s3_prefix
531
+
532
+ if updates:
533
+ for k, v in updates.items():
534
+ setattr(domain, k, v)
535
+ domain.save(update_fields=list(updates.keys()) + ["modified"])
536
+
537
+ return ReconcileResult(
538
+ domain=domain.name,
539
+ region=region,
540
+ topic_arns=result.topic_arns,
541
+ receipt_rule=result.receipt_rule,
542
+ rule_set=result.rule_set,
543
+ notes=result.notes,
544
+ )
545
+
546
+ except Exception as e:
547
+ logger.error(f"reconcile error for domain {domain.name}: {e}")
548
+ raise
@@ -0,0 +1,6 @@
1
+ """
2
+ DocIt - Documentation Management System
3
+
4
+ A Django-MOJO app for creating, organizing, and managing documentation
5
+ with hierarchical pages, version control, and asset management.
6
+ """
@@ -0,0 +1,25 @@
1
+ from pygments import highlight
2
+ from pygments.lexers import get_lexer_by_name
3
+ from pygments.formatters import HtmlFormatter
4
+ from mistune.util import escape_html
5
+
6
+ def _render_fenced_code_with_highlight(renderer, token, state):
7
+ lang = token['attrs'].get('lang')
8
+ code = token['text']
9
+
10
+ if not lang:
11
+ return f'<pre><code>{escape_html(code)}</code></pre>\n'
12
+ try:
13
+ lexer = get_lexer_by_name(lang, stripall=True)
14
+ formatter = HtmlFormatter()
15
+ return highlight(code, lexer, formatter)
16
+ except Exception:
17
+ return f'<pre><code class="language-{lang}">{escape_html(code)}</code></pre>\n'
18
+
19
+ def plugin_highlight(md):
20
+ """
21
+ A mistune v3 plugin for syntax highlighting of fenced code blocks.
22
+ """
23
+ md.renderer.register('fenced_code', _render_fenced_code_with_highlight)
24
+
25
+ plugin = plugin_highlight
@@ -0,0 +1,12 @@
1
+ def plugin_toc(md):
2
+ """
3
+ A mistune v3 plugin to support a table of contents placeholder [TOC].
4
+ This uses a render hook to perform a simple text substitution.
5
+ """
6
+ def before_render_hook(renderer, text, state):
7
+ # Simple text replacement for the [TOC] placeholder
8
+ return text.replace('[TOC]', '<div class="toc"></div>')
9
+
10
+ md.before_render_hooks.append(before_render_hook)
11
+
12
+ plugin = plugin_toc