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,206 @@
1
+ # Generated by Django 4.2.21 on 2025-08-27 18:23
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+ import mojo.models.rest
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name='EmailAttachment',
18
+ fields=[
19
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
+ ('created', models.DateTimeField(auto_now_add=True)),
21
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
22
+ ('filename', models.CharField(blank=True, help_text='Original filename (if provided by the sender)', max_length=512, null=True)),
23
+ ('content_type', models.CharField(blank=True, help_text='MIME content type (e.g., application/pdf)', max_length=255, null=True)),
24
+ ('size_bytes', models.IntegerField(default=0, help_text='Size of the stored attachment in bytes (approximate)')),
25
+ ('stored_as', models.CharField(help_text='Storage reference (e.g., s3://bucket/key)', max_length=512)),
26
+ ('metadata', models.JSONField(blank=True, default=dict, help_text='Arbitrary metadata (e.g., content-id, part headers)')),
27
+ ],
28
+ options={
29
+ 'db_table': 'aws_email_attachment',
30
+ 'ordering': ['-created', 'id'],
31
+ },
32
+ bases=(models.Model, mojo.models.rest.MojoModel),
33
+ ),
34
+ migrations.CreateModel(
35
+ name='EmailDomain',
36
+ fields=[
37
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
38
+ ('mojo_secrets', models.TextField(blank=True, default=None, null=True)),
39
+ ('created', models.DateTimeField(auto_now_add=True)),
40
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
41
+ ('name', models.CharField(db_index=True, max_length=255, unique=True)),
42
+ ('region', models.CharField(default='us-west-2', help_text='AWS region for SES operations', max_length=64)),
43
+ ('status', models.CharField(db_index=True, default='pending', help_text='High-level status: pending, verified, error (free-form)', max_length=32)),
44
+ ('receiving_enabled', models.BooleanField(default=False, help_text='When true, domain-level catch-all receiving is enabled via SES receipt rules')),
45
+ ('s3_inbound_bucket', models.CharField(blank=True, help_text='S3 bucket for inbound emails (required if receiving_enabled)', max_length=255, null=True)),
46
+ ('s3_inbound_prefix', models.CharField(blank=True, default='', help_text='S3 prefix for inbound emails (e.g., inbound/example.com/)', max_length=255)),
47
+ ('dns_mode', models.CharField(default='manual', help_text='DNS automation mode: manual | route53 | godaddy', max_length=32)),
48
+ ('sns_topic_bounce_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES bounce notifications', max_length=512, null=True)),
49
+ ('sns_topic_complaint_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES complaint notifications', max_length=512, null=True)),
50
+ ('sns_topic_delivery_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES delivery notifications', max_length=512, null=True)),
51
+ ('sns_topic_inbound_arn', models.CharField(blank=True, help_text='SNS topic ARN for SES inbound notifications', max_length=512, null=True)),
52
+ ('metadata', models.JSONField(blank=True, default=dict)),
53
+ ],
54
+ options={
55
+ 'db_table': 'aws_email_domain',
56
+ 'ordering': ['name'],
57
+ },
58
+ bases=(models.Model, mojo.models.rest.MojoModel),
59
+ ),
60
+ migrations.CreateModel(
61
+ name='Mailbox',
62
+ fields=[
63
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
64
+ ('created', models.DateTimeField(auto_now_add=True)),
65
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
66
+ ('email', models.EmailField(db_index=True, help_text='Full email address for this mailbox (e.g., support@example.com)', max_length=254, unique=True)),
67
+ ('allow_inbound', models.BooleanField(default=True, help_text='If true, inbound messages addressed to this mailbox will be processed')),
68
+ ('allow_outbound', models.BooleanField(default=True, help_text='If true, outbound messages can be sent from this mailbox')),
69
+ ('async_handler', models.CharField(blank=True, help_text="Dotted path to async handler: 'package.module:function'", max_length=255, null=True)),
70
+ ('metadata', models.JSONField(blank=True, default=dict)),
71
+ ('domain', models.ForeignKey(help_text='Owning email domain (SES identity)', on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to='aws.emaildomain')),
72
+ ],
73
+ options={
74
+ 'db_table': 'aws_mailbox',
75
+ 'ordering': ['email'],
76
+ },
77
+ bases=(models.Model, mojo.models.rest.MojoModel),
78
+ ),
79
+ migrations.CreateModel(
80
+ name='SentMessage',
81
+ fields=[
82
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
83
+ ('created', models.DateTimeField(auto_now_add=True)),
84
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
85
+ ('ses_message_id', models.CharField(blank=True, db_index=True, help_text='AWS SES MessageId returned after a successful send', max_length=255, null=True)),
86
+ ('to_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses (To)')),
87
+ ('cc_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses (Cc)')),
88
+ ('bcc_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses (Bcc)')),
89
+ ('subject', models.CharField(blank=True, help_text='Email subject', max_length=512, null=True)),
90
+ ('body_text', models.TextField(blank=True, help_text='Plain text body', null=True)),
91
+ ('body_html', models.TextField(blank=True, help_text='HTML body', null=True)),
92
+ ('template_name', models.CharField(blank=True, help_text='Optional EmailTemplate name used to render this message', max_length=255, null=True)),
93
+ ('template_context', models.JSONField(blank=True, default=dict, help_text='Context used when rendering a template')),
94
+ ('status', models.CharField(choices=[('queued', 'Queued'), ('sending', 'Sending'), ('delivered', 'Delivered'), ('bounced', 'Bounced'), ('complained', 'Complained'), ('failed', 'Failed'), ('unknown', 'Unknown')], db_index=True, default='queued', help_text='Current delivery status', max_length=32)),
95
+ ('status_reason', models.TextField(blank=True, help_text='Details or raw payload for bounces/complaints/errors', null=True)),
96
+ ('metadata', models.JSONField(blank=True, default=dict, help_text='Arbitrary metadata for downstream processing/auditing')),
97
+ ('mailbox', models.ForeignKey(help_text='Mailbox used as the sender (envelope MAIL FROM = mailbox.email)', on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to='aws.mailbox')),
98
+ ],
99
+ options={
100
+ 'db_table': 'aws_sent_message',
101
+ 'ordering': ['-created', 'id'],
102
+ },
103
+ bases=(models.Model, mojo.models.rest.MojoModel),
104
+ ),
105
+ migrations.CreateModel(
106
+ name='IncomingEmail',
107
+ fields=[
108
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
109
+ ('created', models.DateTimeField(auto_now_add=True)),
110
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
111
+ ('s3_object_url', models.CharField(help_text='S3 URL for the raw MIME message (e.g., s3://bucket/key)', max_length=512)),
112
+ ('message_id', models.CharField(blank=True, db_index=True, help_text='SMTP Message-ID header (if present)', max_length=255, null=True)),
113
+ ('from_address', models.CharField(blank=True, help_text='Raw From header address (may include name)', max_length=512, null=True)),
114
+ ('to_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses from To header')),
115
+ ('cc_addresses', models.JSONField(blank=True, default=list, help_text='List of recipient addresses from Cc header')),
116
+ ('subject', models.CharField(blank=True, help_text='Email subject', max_length=512, null=True)),
117
+ ('date_header', models.DateTimeField(blank=True, help_text='Parsed Date header from the message', null=True)),
118
+ ('headers', models.JSONField(blank=True, default=dict, help_text='All headers as a JSON object (flattened)')),
119
+ ('text_body', models.TextField(blank=True, help_text='Extracted plain text body (if available)', null=True)),
120
+ ('html_body', models.TextField(blank=True, help_text='Extracted HTML body (if available)', null=True)),
121
+ ('size_bytes', models.IntegerField(default=0, help_text='Approximate size of the raw message in bytes')),
122
+ ('received_at', models.DateTimeField(blank=True, db_index=True, help_text='Time message was received (from SNS/S3 event or set by parser)', null=True)),
123
+ ('processed', models.BooleanField(default=False, help_text='True if post-receive processing completed')),
124
+ ('process_status', models.CharField(db_index=True, default='pending', help_text='Processing status: pending | success | error', max_length=32)),
125
+ ('process_error', models.TextField(blank=True, help_text='Error details if processing failed', null=True)),
126
+ ('mailbox', models.ForeignKey(blank=True, help_text='Associated mailbox if any recipient matches', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='incoming_emails', to='aws.mailbox')),
127
+ ],
128
+ options={
129
+ 'db_table': 'aws_incoming_email',
130
+ 'ordering': ['-received_at', '-created'],
131
+ },
132
+ bases=(models.Model, mojo.models.rest.MojoModel),
133
+ ),
134
+ migrations.CreateModel(
135
+ name='EmailTemplate',
136
+ fields=[
137
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
138
+ ('created', models.DateTimeField(auto_now_add=True)),
139
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
140
+ ('name', models.CharField(db_index=True, help_text='Unique template name (used by callers to reference this template)', max_length=255, unique=True)),
141
+ ('subject_template', models.TextField(blank=True, default='', help_text='Django template string for the email subject')),
142
+ ('html_template', models.TextField(blank=True, default='', help_text='Django template string for the HTML body')),
143
+ ('text_template', models.TextField(blank=True, default='', help_text='Django template string for the plain text body')),
144
+ ('metadata', models.JSONField(blank=True, default=dict, help_text='Arbitrary metadata for this template (e.g., description, tags)')),
145
+ ],
146
+ options={
147
+ 'db_table': 'aws_email_template',
148
+ 'ordering': ['name'],
149
+ 'indexes': [models.Index(fields=['modified'], name='aws_email_t_modifie_cf76d6_idx'), models.Index(fields=['name'], name='aws_email_t_name_d1662a_idx')],
150
+ },
151
+ bases=(models.Model, mojo.models.rest.MojoModel),
152
+ ),
153
+ migrations.AddIndex(
154
+ model_name='emaildomain',
155
+ index=models.Index(fields=['status'], name='aws_email_d_status_398945_idx'),
156
+ ),
157
+ migrations.AddIndex(
158
+ model_name='emaildomain',
159
+ index=models.Index(fields=['modified'], name='aws_email_d_modifie_66ca75_idx'),
160
+ ),
161
+ migrations.AddField(
162
+ model_name='emailattachment',
163
+ name='incoming_email',
164
+ field=models.ForeignKey(help_text='The inbound email this attachment belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='aws.incomingemail'),
165
+ ),
166
+ migrations.AddIndex(
167
+ model_name='sentmessage',
168
+ index=models.Index(fields=['modified'], name='aws_sent_me_modifie_a26352_idx'),
169
+ ),
170
+ migrations.AddIndex(
171
+ model_name='sentmessage',
172
+ index=models.Index(fields=['status'], name='aws_sent_me_status_63e619_idx'),
173
+ ),
174
+ migrations.AddIndex(
175
+ model_name='sentmessage',
176
+ index=models.Index(fields=['ses_message_id'], name='aws_sent_me_ses_mes_b72855_idx'),
177
+ ),
178
+ migrations.AddIndex(
179
+ model_name='mailbox',
180
+ index=models.Index(fields=['modified'], name='aws_mailbox_modifie_597f5d_idx'),
181
+ ),
182
+ migrations.AddIndex(
183
+ model_name='mailbox',
184
+ index=models.Index(fields=['email'], name='aws_mailbox_email_e2c6d1_idx'),
185
+ ),
186
+ migrations.AddIndex(
187
+ model_name='incomingemail',
188
+ index=models.Index(fields=['modified'], name='aws_incomin_modifie_8e68a9_idx'),
189
+ ),
190
+ migrations.AddIndex(
191
+ model_name='incomingemail',
192
+ index=models.Index(fields=['received_at'], name='aws_incomin_receive_7b6d93_idx'),
193
+ ),
194
+ migrations.AddIndex(
195
+ model_name='incomingemail',
196
+ index=models.Index(fields=['message_id'], name='aws_incomin_message_d8ca7f_idx'),
197
+ ),
198
+ migrations.AddIndex(
199
+ model_name='emailattachment',
200
+ index=models.Index(fields=['modified'], name='aws_email_a_modifie_b283df_idx'),
201
+ ),
202
+ migrations.AddIndex(
203
+ model_name='emailattachment',
204
+ index=models.Index(fields=['filename'], name='aws_email_a_filenam_da3c89_idx'),
205
+ ),
206
+ ]
@@ -0,0 +1,28 @@
1
+ # Generated by Django 4.2.21 on 2025-08-27 21:38
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('aws', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='emaildomain',
15
+ name='can_recv',
16
+ field=models.BooleanField(default=False, help_text='True if inbound receiving is ready per last audit'),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='emaildomain',
20
+ name='can_send',
21
+ field=models.BooleanField(default=False, help_text='True if outbound sending is ready per last audit'),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='emaildomain',
25
+ name='status',
26
+ field=models.CharField(db_index=True, default='pending', help_text='Domain status: "pending" (created), "ready" (audit passed), or "missing" (audit failed)', max_length=32),
27
+ ),
28
+ ]
@@ -0,0 +1,31 @@
1
+ # Generated by Django 4.2.21 on 2025-08-31 23:09
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('aws', '0002_emaildomain_can_recv_emaildomain_can_send_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='mailbox',
15
+ name='is_domain_default',
16
+ field=models.BooleanField(db_index=True, default=False, help_text='Default mailbox for this domain (one per domain)'),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='mailbox',
20
+ name='is_system_default',
21
+ field=models.BooleanField(db_index=True, default=False, help_text='System-wide default mailbox (only one allowed)'),
22
+ ),
23
+ migrations.AddIndex(
24
+ model_name='mailbox',
25
+ index=models.Index(fields=['is_system_default'], name='aws_mailbox_is_syst_9911c7_idx'),
26
+ ),
27
+ migrations.AddIndex(
28
+ model_name='mailbox',
29
+ index=models.Index(fields=['is_domain_default', 'domain'], name='aws_mailbox_is_doma_189efb_idx'),
30
+ ),
31
+ ]
@@ -0,0 +1,39 @@
1
+ # Generated by Django 4.2.23 on 2025-09-06 00:00
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+ import mojo.models.rest
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
+ ('account', '0014_notificationdelivery_data_payload_and_more'),
14
+ ('aws', '0003_mailbox_is_domain_default_mailbox_is_system_default_and_more'),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='S3Bucket',
20
+ fields=[
21
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+ ('mojo_secrets', models.TextField(blank=True, default=None, null=True)),
23
+ ('created', models.DateTimeField(auto_now_add=True)),
24
+ ('modified', models.DateTimeField(auto_now=True, db_index=True)),
25
+ ('name', models.CharField(db_index=True, max_length=255, unique=True)),
26
+ ('region', models.CharField(default='us-west-2', help_text='AWS region for SES operations', max_length=64)),
27
+ ('metadata', models.JSONField(blank=True, default=dict)),
28
+ ('is_active', models.BooleanField(db_index=True, default=False)),
29
+ ('is_system_default', models.BooleanField(db_index=True, default=False)),
30
+ ('is_group_default', models.BooleanField(db_index=True, default=False)),
31
+ ('group', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s3_buckets', to='account.group')),
32
+ ('user', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s3_buckets', to=settings.AUTH_USER_MODEL)),
33
+ ],
34
+ options={
35
+ 'abstract': False,
36
+ },
37
+ bases=(models.Model, mojo.models.rest.MojoModel),
38
+ ),
39
+ ]
@@ -0,0 +1,21 @@
1
+ # Generated by Django 4.2.23 on 2025-09-06 00:10
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('aws', '0004_s3bucket'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='emaildomain',
15
+ name='region',
16
+ field=models.CharField(default='us-east-1', help_text='AWS region for SES operations', max_length=64),
17
+ ),
18
+ migrations.DeleteModel(
19
+ name='S3Bucket',
20
+ ),
21
+ ]
@@ -0,0 +1,19 @@
1
+ """
2
+ AWS models package exports
3
+ """
4
+
5
+ from .email_domain import EmailDomain
6
+ from .mailbox import Mailbox
7
+ from .incoming_email import IncomingEmail
8
+ from .email_attachment import EmailAttachment
9
+ from .sent_message import SentMessage
10
+ from .email_template import EmailTemplate
11
+
12
+ __all__ = [
13
+ "EmailDomain",
14
+ "Mailbox",
15
+ "IncomingEmail",
16
+ "EmailAttachment",
17
+ "SentMessage",
18
+ "EmailTemplate",
19
+ ]
@@ -0,0 +1,99 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+
4
+
5
+ class EmailAttachment(models.Model, MojoModel):
6
+ """
7
+ EmailAttachment
8
+
9
+ Represents a single attachment extracted from an incoming email. The binary
10
+ content is stored in the same inbound S3 bucket as the raw message, and
11
+ referenced by `stored_as` (e.g., s3://bucket/key).
12
+
13
+ Relationships:
14
+ - incoming_email: FK to IncomingEmail
15
+ """
16
+
17
+ created = models.DateTimeField(auto_now_add=True, editable=False)
18
+ modified = models.DateTimeField(auto_now=True, db_index=True)
19
+
20
+ incoming_email = models.ForeignKey(
21
+ "aws.IncomingEmail",
22
+ related_name="attachments",
23
+ on_delete=models.CASCADE,
24
+ help_text="The inbound email this attachment belongs to"
25
+ )
26
+
27
+ filename = models.CharField(
28
+ max_length=512,
29
+ null=True,
30
+ blank=True,
31
+ help_text="Original filename (if provided by the sender)"
32
+ )
33
+
34
+ content_type = models.CharField(
35
+ max_length=255,
36
+ null=True,
37
+ blank=True,
38
+ help_text="MIME content type (e.g., application/pdf)"
39
+ )
40
+
41
+ size_bytes = models.IntegerField(
42
+ default=0,
43
+ help_text="Size of the stored attachment in bytes (approximate)"
44
+ )
45
+
46
+ stored_as = models.CharField(
47
+ max_length=512,
48
+ help_text="Storage reference (e.g., s3://bucket/key)"
49
+ )
50
+
51
+ metadata = models.JSONField(
52
+ default=dict,
53
+ blank=True,
54
+ help_text="Arbitrary metadata (e.g., content-id, part headers)"
55
+ )
56
+
57
+ class Meta:
58
+ db_table = "aws_email_attachment"
59
+ indexes = [
60
+ models.Index(fields=["modified"]),
61
+ models.Index(fields=["filename"]),
62
+ ]
63
+ ordering = ["-created", "id"]
64
+
65
+ class RestMeta:
66
+ VIEW_PERMS = ["manage_aws"]
67
+ SAVE_PERMS = ["manage_aws"]
68
+ DELETE_PERMS = ["manage_aws"]
69
+ SEARCH_FIELDS = ["filename", "content_type", "stored_as"]
70
+ GRAPHS = {
71
+ "basic": {
72
+ "fields": [
73
+ "id",
74
+ "incoming_email",
75
+ "filename",
76
+ "content_type",
77
+ "size_bytes",
78
+ "created",
79
+ ],
80
+ "graphs": {"incoming_email": "basic"},
81
+ },
82
+ "default": {
83
+ "fields": [
84
+ "id",
85
+ "incoming_email",
86
+ "filename",
87
+ "content_type",
88
+ "size_bytes",
89
+ "stored_as",
90
+ "metadata",
91
+ "created",
92
+ "modified",
93
+ ],
94
+ "graphs": {"incoming_email": "basic"},
95
+ },
96
+ }
97
+
98
+ def __str__(self) -> str:
99
+ return self.filename or self.stored_as or f"Attachment {self.pk}"
@@ -0,0 +1,218 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel, MojoSecrets
3
+ from mojo.helpers.settings import settings
4
+ from mojo.helpers.aws.ses_domain import audit_domain_config, reconcile_domain_config
5
+ from mojo.helpers import aws
6
+
7
+
8
+ class EmailDomain(MojoSecrets, MojoModel):
9
+ """
10
+ EmailDomain
11
+
12
+ Minimal model for managing an SES-backed email domain configuration in MOJO.
13
+
14
+ Notes:
15
+ - 'name' is the domain (e.g., example.com).
16
+ - 'region' defaults to project AWS region (settings.AWS_REGION) or 'us-east-1'.
17
+ - 'receiving_enabled' toggles domain-level catch-all receiving (SES receipt rules).
18
+ - 's3_inbound_bucket' and 's3_inbound_prefix' identify where inbound emails are stored.
19
+ - 'status' is a lightweight lifecycle indicator: pending | verified | error (free-form for now).
20
+ - 'metadata' allows flexible per-domain extension without schema churn.
21
+ """
22
+
23
+ created = models.DateTimeField(auto_now_add=True, editable=False)
24
+ modified = models.DateTimeField(auto_now=True, db_index=True)
25
+
26
+ name = models.CharField(max_length=255, unique=True, db_index=True)
27
+ region = models.CharField(
28
+ max_length=64,
29
+ default=getattr(settings, 'AWS_REGION', 'us-east-1'),
30
+ help_text="AWS region for SES operations"
31
+ )
32
+
33
+ status = models.CharField(
34
+ max_length=32,
35
+ default='pending',
36
+ db_index=True,
37
+ help_text='Domain status: "pending" (created), "ready" (audit passed), or "missing" (audit failed)'
38
+ )
39
+
40
+ receiving_enabled = models.BooleanField(
41
+ default=False,
42
+ help_text="When true, domain-level catch-all receiving is enabled via SES receipt rules"
43
+ )
44
+
45
+ s3_inbound_bucket = models.CharField(
46
+ max_length=255,
47
+ null=True,
48
+ blank=True,
49
+ help_text="S3 bucket for inbound emails (required if receiving_enabled)"
50
+ )
51
+ s3_inbound_prefix = models.CharField(
52
+ max_length=255,
53
+ default='',
54
+ blank=True,
55
+ help_text="S3 prefix for inbound emails (e.g., inbound/example.com/)"
56
+ )
57
+
58
+ dns_mode = models.CharField(
59
+ max_length=32,
60
+ default='manual',
61
+ help_text="DNS automation mode: manual | route53 | godaddy"
62
+ )
63
+
64
+ sns_topic_bounce_arn = models.CharField(
65
+ max_length=512,
66
+ null=True,
67
+ blank=True,
68
+ help_text="SNS topic ARN for SES bounce notifications"
69
+ )
70
+ sns_topic_complaint_arn = models.CharField(
71
+ max_length=512,
72
+ null=True,
73
+ blank=True,
74
+ help_text="SNS topic ARN for SES complaint notifications"
75
+ )
76
+ sns_topic_delivery_arn = models.CharField(
77
+ max_length=512,
78
+ null=True,
79
+ blank=True,
80
+ help_text="SNS topic ARN for SES delivery notifications"
81
+ )
82
+ sns_topic_inbound_arn = models.CharField(
83
+ max_length=512,
84
+ null=True,
85
+ blank=True,
86
+ help_text="SNS topic ARN for SES inbound notifications"
87
+ )
88
+
89
+ metadata = models.JSONField(default=dict, blank=True)
90
+
91
+ # Computed readiness flags updated by audit runs
92
+ can_send = models.BooleanField(
93
+ default=False,
94
+ help_text="True if outbound sending is ready per last audit"
95
+ )
96
+ can_recv = models.BooleanField(
97
+ default=False,
98
+ help_text="True if inbound receiving is ready per last audit"
99
+ )
100
+
101
+ class Meta:
102
+ db_table = "aws_email_domain"
103
+ indexes = [
104
+ models.Index(fields=["status"]),
105
+ models.Index(fields=["modified"]),
106
+ ]
107
+ ordering = ["name"]
108
+
109
+ class RestMeta:
110
+ VIEW_PERMS = ["manage_aws"]
111
+ SAVE_PERMS = ["manage_aws"]
112
+ DELETE_PERMS = ["manage_aws"]
113
+ SEARCH_FIELDS = ["name", "region", "status"]
114
+ GRAPHS = {
115
+ "basic": {
116
+ "fields": [
117
+ "id",
118
+ "name",
119
+ "region",
120
+ "status",
121
+ "receiving_enabled",
122
+ ]
123
+ },
124
+ "default": {
125
+ "fields": [
126
+ "id",
127
+ "name",
128
+ "region",
129
+ "status",
130
+ "receiving_enabled",
131
+ "s3_inbound_bucket",
132
+ "s3_inbound_prefix",
133
+ "dns_mode",
134
+ "sns_topic_bounce_arn",
135
+ "sns_topic_complaint_arn",
136
+ "sns_topic_delivery_arn",
137
+ "sns_topic_inbound_arn",
138
+ "metadata",
139
+ "created",
140
+ "modified",
141
+ ],
142
+ "extra": [
143
+ "aws_key",
144
+ "aws_secret_masked"
145
+ ]
146
+ },
147
+ }
148
+
149
+ @property
150
+ def aws_key(self):
151
+ return self.get_secret('aws_key')
152
+
153
+ @property
154
+ def aws_secret(self):
155
+ return self.get_secret('aws_secret')
156
+
157
+ @property
158
+ def aws_secret_masked(self):
159
+ secret = self.get_secret('aws_secret', '')
160
+ if len(secret) > 4:
161
+ return '*' * (len(secret) - 4) + secret[-4:]
162
+ return secret
163
+
164
+
165
+ @property
166
+ def aws_region(self):
167
+ return self.region or getattr(settings, 'AWS_REGION', 'us-east-1')
168
+
169
+ @property
170
+ def is_verified(self):
171
+ return self.status in ["verified", "ready"]
172
+
173
+ def set_aws_key(self, key):
174
+ self.set_secret('aws_key', key)
175
+
176
+ def set_aws_secret(self, secret):
177
+ self.set_secret('aws_secret', secret)
178
+
179
+ def on_rest_created(self):
180
+ """
181
+ Automatically audit and reconcile SES/SNS configuration after this domain is created.
182
+ This keeps AWS-side resources aligned without requiring a separate call.
183
+ """
184
+ try:
185
+ region = self.aws_region
186
+ # Audit current state (best-effort; ignore failures)
187
+ try:
188
+ desired_receiving = None
189
+ if self.receiving_enabled and self.s3_inbound_bucket:
190
+ desired_receiving = {
191
+ 'bucket': self.s3_inbound_bucket,
192
+ 'prefix': self.s3_inbound_prefix or '',
193
+ 'rule_set': 'mojo-default-receiving',
194
+ 'rule_name': f'mojo-{self.name}-catchall',
195
+ }
196
+ audit_domain_config(
197
+ domain=self.name,
198
+ region=region,
199
+ desired_receiving=desired_receiving,
200
+ )
201
+ except Exception:
202
+ # Non-fatal: continue with reconcile
203
+ pass
204
+
205
+ # Reconcile (idempotent): ensure topics/mappings and catch-all receipt rule if enabled
206
+ reconcile_domain_config(
207
+ domain=self.name,
208
+ region=region,
209
+ receiving_enabled=self.receiving_enabled,
210
+ s3_bucket=self.s3_inbound_bucket,
211
+ s3_prefix=self.s3_inbound_prefix or '',
212
+ )
213
+ except Exception:
214
+ # Swallow exceptions to avoid failing the create call; details can be inspected via /audit
215
+ pass
216
+
217
+ def __str__(self) -> str:
218
+ return self.name