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,483 @@
1
+ """
2
+ AWS SES Helper Module
3
+
4
+ Provides simple interfaces for managing AWS SES (Simple Email Service).
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import boto3
10
+ import botocore
11
+ from typing import Dict, List, Optional, Union, Any
12
+
13
+ from .client import get_session
14
+ from mojo.helpers.settings import settings
15
+ from mojo.helpers import logit
16
+
17
+ logger = logit.get_logger(__name__)
18
+
19
+
20
+ class EmailSender:
21
+ """
22
+ Simple interface for sending emails using AWS SES.
23
+ """
24
+
25
+ def __init__(self, access_key: Optional[str] = None, secret_key: Optional[str] = None,
26
+ region: Optional[str] = None):
27
+ """
28
+ Initialize SES client with credentials.
29
+
30
+ Args:
31
+ access_key: AWS access key, defaults to settings.AWS_KEY
32
+ secret_key: AWS secret key, defaults to settings.AWS_SECRET
33
+ region: AWS region, defaults to settings.AWS_REGION if available
34
+ """
35
+ self.access_key = access_key or settings.AWS_KEY
36
+ self.secret_key = secret_key or settings.AWS_SECRET
37
+ self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
38
+
39
+ session = get_session(self.access_key, self.secret_key, self.region)
40
+ self.client = session.client('ses')
41
+
42
+ def send_email(self,
43
+ source: str,
44
+ to_addresses: List[str],
45
+ subject: str,
46
+ body_text: Optional[str] = None,
47
+ body_html: Optional[str] = None,
48
+ cc_addresses: Optional[List[str]] = None,
49
+ bcc_addresses: Optional[List[str]] = None,
50
+ reply_to_addresses: Optional[List[str]] = None) -> Dict:
51
+ """
52
+ Send an email using Amazon SES.
53
+
54
+ Args:
55
+ source: Email sender address
56
+ to_addresses: List of recipient email addresses
57
+ subject: Email subject
58
+ body_text: Plain text email body
59
+ body_html: HTML email body
60
+ cc_addresses: List of CC email addresses
61
+ bcc_addresses: List of BCC email addresses
62
+ reply_to_addresses: List of reply-to email addresses
63
+
64
+ Returns:
65
+ Dict containing 'MessageId' if successful
66
+ """
67
+ if not body_text and not body_html:
68
+ raise ValueError("At least one of body_text or body_html must be provided")
69
+
70
+ message = {
71
+ 'Subject': {'Data': subject}
72
+ }
73
+
74
+ # Add body content
75
+ body = {}
76
+ if body_text:
77
+ body['Text'] = {'Data': body_text}
78
+ if body_html:
79
+ body['Html'] = {'Data': body_html}
80
+ message['Body'] = body
81
+
82
+ # Configure recipients
83
+ destination = {'ToAddresses': to_addresses}
84
+ if cc_addresses:
85
+ destination['CcAddresses'] = cc_addresses
86
+ if bcc_addresses:
87
+ destination['BccAddresses'] = bcc_addresses
88
+
89
+ # Prepare send parameters
90
+ send_params = {
91
+ 'Source': source,
92
+ 'Destination': destination,
93
+ 'Message': message
94
+ }
95
+
96
+ if reply_to_addresses:
97
+ send_params['ReplyToAddresses'] = reply_to_addresses
98
+
99
+ try:
100
+ response = self.client.send_email(**send_params)
101
+ logger.info(f"Email sent successfully with MessageId: {response['MessageId']}")
102
+ return response
103
+ except botocore.exceptions.ClientError as e:
104
+ logger.error(f"Failed to send email: {e}")
105
+ return {'Error': str(e)}
106
+
107
+ def send_template_email(self,
108
+ source: str,
109
+ to_addresses: List[str],
110
+ template_name: str,
111
+ template_data: Dict,
112
+ cc_addresses: Optional[List[str]] = None,
113
+ bcc_addresses: Optional[List[str]] = None,
114
+ reply_to_addresses: Optional[List[str]] = None) -> Dict:
115
+ """
116
+ Send an email using an SES template.
117
+
118
+ Args:
119
+ source: Email sender address
120
+ to_addresses: List of recipient email addresses
121
+ template_name: Name of the SES template to use
122
+ template_data: Dictionary of template data
123
+ cc_addresses: List of CC email addresses
124
+ bcc_addresses: List of BCC email addresses
125
+ reply_to_addresses: List of reply-to email addresses
126
+
127
+ Returns:
128
+ Dict containing 'MessageId' if successful
129
+ """
130
+ # Configure recipients
131
+ destination = {'ToAddresses': to_addresses}
132
+ if cc_addresses:
133
+ destination['CcAddresses'] = cc_addresses
134
+ if bcc_addresses:
135
+ destination['BccAddresses'] = bcc_addresses
136
+
137
+ # Prepare send parameters
138
+ send_params = {
139
+ 'Source': source,
140
+ 'Destination': destination,
141
+ 'Template': template_name,
142
+ 'TemplateData': json.dumps(template_data)
143
+ }
144
+
145
+ if reply_to_addresses:
146
+ send_params['ReplyToAddresses'] = reply_to_addresses
147
+
148
+ try:
149
+ response = self.client.send_templated_email(**send_params)
150
+ logger.info(f"Template email sent successfully with MessageId: {response['MessageId']}")
151
+ return response
152
+ except botocore.exceptions.ClientError as e:
153
+ logger.error(f"Failed to send template email: {e}")
154
+ return {'Error': str(e)}
155
+
156
+ def send_raw_email(self, raw_message: str, source: Optional[str] = None) -> Dict:
157
+ """
158
+ Send a raw email (MIME format).
159
+
160
+ Args:
161
+ raw_message: The raw text of the message
162
+ source: Email sender address (optional, can be specified in raw_message)
163
+
164
+ Returns:
165
+ Dict containing 'MessageId' if successful
166
+ """
167
+ send_params = {
168
+ 'RawMessage': {'Data': raw_message}
169
+ }
170
+
171
+ if source:
172
+ send_params['Source'] = source
173
+
174
+ try:
175
+ response = self.client.send_raw_email(**send_params)
176
+ logger.info(f"Raw email sent successfully with MessageId: {response['MessageId']}")
177
+ return response
178
+ except botocore.exceptions.ClientError as e:
179
+ logger.error(f"Failed to send raw email: {e}")
180
+ return {'Error': str(e)}
181
+
182
+ def get_send_quota(self) -> Dict:
183
+ """
184
+ Get SES sending limits and usage.
185
+
186
+ Returns:
187
+ Dict with quota information
188
+ """
189
+ try:
190
+ return self.client.get_send_quota()
191
+ except botocore.exceptions.ClientError as e:
192
+ logger.error(f"Failed to get send quota: {e}")
193
+ return {'Error': str(e)}
194
+
195
+ def verify_email_identity(self, email: str) -> bool:
196
+ """
197
+ Verify an email address identity.
198
+
199
+ Args:
200
+ email: Email address to verify
201
+
202
+ Returns:
203
+ True if verification process started successfully
204
+ """
205
+ try:
206
+ self.client.verify_email_identity(EmailAddress=email)
207
+ logger.info(f"Verification email sent to {email}")
208
+ return True
209
+ except botocore.exceptions.ClientError as e:
210
+ logger.error(f"Failed to initiate verification for {email}: {e}")
211
+ return False
212
+
213
+ def verify_domain_identity(self, domain: str) -> Dict:
214
+ """
215
+ Verify a domain identity.
216
+
217
+ Args:
218
+ domain: Domain name to verify
219
+
220
+ Returns:
221
+ Dict with verification token if successful
222
+ """
223
+ try:
224
+ response = self.client.verify_domain_identity(Domain=domain)
225
+ logger.info(f"Domain verification initiated for {domain}")
226
+ return response
227
+ except botocore.exceptions.ClientError as e:
228
+ logger.error(f"Failed to initiate domain verification for {domain}: {e}")
229
+ return {'Error': str(e)}
230
+
231
+ def list_identities(self, identity_type: Optional[str] = None) -> List[str]:
232
+ """
233
+ List verified email addresses and domains.
234
+
235
+ Args:
236
+ identity_type: Filter by type ('EmailAddress' or 'Domain')
237
+
238
+ Returns:
239
+ List of identity strings
240
+ """
241
+ try:
242
+ params = {}
243
+ if identity_type:
244
+ params['IdentityType'] = identity_type
245
+
246
+ response = self.client.list_identities(**params)
247
+ return response.get('Identities', [])
248
+ except botocore.exceptions.ClientError as e:
249
+ logger.error(f"Failed to list identities: {e}")
250
+ return []
251
+
252
+
253
+ class EmailTemplate:
254
+ """
255
+ Simple interface for managing SES email templates.
256
+ """
257
+
258
+ def __init__(self, name: str, access_key: Optional[str] = None,
259
+ secret_key: Optional[str] = None, region: Optional[str] = None):
260
+ """
261
+ Initialize a template manager for the specified SES template.
262
+
263
+ Args:
264
+ name: The template name
265
+ access_key: AWS access key, defaults to settings.AWS_KEY
266
+ secret_key: AWS secret key, defaults to settings.AWS_SECRET
267
+ region: AWS region, defaults to settings.AWS_REGION if available
268
+ """
269
+ self.name = name
270
+ self.access_key = access_key or settings.AWS_KEY
271
+ self.secret_key = secret_key or settings.AWS_SECRET
272
+ self.region = region or getattr(settings, 'AWS_REGION', 'us-east-1')
273
+
274
+ session = get_session(self.access_key, self.secret_key, self.region)
275
+ self.client = session.client('ses')
276
+ self.exists = self._check_exists()
277
+
278
+ def _check_exists(self) -> bool:
279
+ """Check if the template exists."""
280
+ try:
281
+ self.client.get_template(TemplateName=self.name)
282
+ return True
283
+ except botocore.exceptions.ClientError as e:
284
+ if e.response['Error']['Code'] == 'TemplateDoesNotExist':
285
+ return False
286
+ logger.error(f"Error checking template existence: {e}")
287
+ raise
288
+
289
+ def create(self, subject: str, html_content: str,
290
+ text_content: Optional[str] = None) -> bool:
291
+ """
292
+ Create an email template.
293
+
294
+ Args:
295
+ subject: Template subject
296
+ html_content: HTML template content
297
+ text_content: Plain text template content
298
+
299
+ Returns:
300
+ True if template was created, False if it already exists
301
+ """
302
+ if self.exists:
303
+ logger.info(f"Template {self.name} already exists")
304
+ return False
305
+
306
+ try:
307
+ template = {
308
+ 'TemplateName': self.name,
309
+ 'SubjectPart': subject,
310
+ 'HtmlPart': html_content,
311
+ }
312
+
313
+ if text_content:
314
+ template['TextPart'] = text_content
315
+
316
+ self.client.create_template(Template=template)
317
+ self.exists = True
318
+ return True
319
+ except botocore.exceptions.ClientError as e:
320
+ logger.error(f"Failed to create template {self.name}: {e}")
321
+ return False
322
+
323
+ def update(self, subject: Optional[str] = None,
324
+ html_content: Optional[str] = None,
325
+ text_content: Optional[str] = None) -> bool:
326
+ """
327
+ Update an existing email template.
328
+
329
+ Args:
330
+ subject: New template subject
331
+ html_content: New HTML template content
332
+ text_content: New plain text template content
333
+
334
+ Returns:
335
+ True if successful, False otherwise
336
+ """
337
+ if not self.exists:
338
+ logger.warning(f"Template {self.name} does not exist")
339
+ return False
340
+
341
+ try:
342
+ # Get current template
343
+ current = self.client.get_template(TemplateName=self.name)['Template']
344
+
345
+ # Update only the specified parts
346
+ template = {
347
+ 'TemplateName': self.name,
348
+ 'SubjectPart': subject or current.get('SubjectPart', ''),
349
+ 'HtmlPart': html_content or current.get('HtmlPart', ''),
350
+ }
351
+
352
+ if text_content or 'TextPart' in current:
353
+ template['TextPart'] = text_content or current.get('TextPart', '')
354
+
355
+ self.client.update_template(Template=template)
356
+ return True
357
+ except botocore.exceptions.ClientError as e:
358
+ logger.error(f"Failed to update template {self.name}: {e}")
359
+ return False
360
+
361
+ def delete(self) -> bool:
362
+ """
363
+ Delete the email template.
364
+
365
+ Returns:
366
+ True if successfully deleted, False otherwise
367
+ """
368
+ if not self.exists:
369
+ logger.info(f"Template {self.name} does not exist")
370
+ return False
371
+
372
+ try:
373
+ self.client.delete_template(TemplateName=self.name)
374
+ self.exists = False
375
+ return True
376
+ except botocore.exceptions.ClientError as e:
377
+ logger.error(f"Failed to delete template {self.name}: {e}")
378
+ return False
379
+
380
+ def get(self) -> Dict:
381
+ """
382
+ Get the email template details.
383
+
384
+ Returns:
385
+ Template details dictionary
386
+ """
387
+ if not self.exists:
388
+ logger.warning(f"Template {self.name} does not exist")
389
+ return {}
390
+
391
+ try:
392
+ response = self.client.get_template(TemplateName=self.name)
393
+ return response.get('Template', {})
394
+ except botocore.exceptions.ClientError as e:
395
+ logger.error(f"Failed to get template {self.name}: {e}")
396
+ return {}
397
+
398
+ @staticmethod
399
+ def list_all_templates() -> List[Dict]:
400
+ """
401
+ List all email templates.
402
+
403
+ Returns:
404
+ List of template information dictionaries
405
+ """
406
+ client = boto3.client('ses',
407
+ aws_access_key_id=settings.AWS_KEY,
408
+ aws_secret_access_key=settings.AWS_SECRET,
409
+ region_name=getattr(settings, 'AWS_REGION', 'us-east-1'))
410
+
411
+ try:
412
+ response = client.list_templates()
413
+ return response.get('TemplatesMetadata', [])
414
+ except botocore.exceptions.ClientError as e:
415
+ logger.error(f"Failed to list templates: {e}")
416
+ return []
417
+
418
+
419
+ # Utility functions
420
+ def send_simple_email(from_email: str, to_email: str, subject: str,
421
+ message: str, html_message: Optional[str] = None) -> Dict:
422
+ """
423
+ Convenience function to send a simple email.
424
+
425
+ Args:
426
+ from_email: Sender email address
427
+ to_email: Recipient email address
428
+ subject: Email subject
429
+ message: Plain text email body
430
+ html_message: Optional HTML email body
431
+
432
+ Returns:
433
+ Response dictionary
434
+ """
435
+ sender = EmailSender()
436
+ return sender.send_email(
437
+ source=from_email,
438
+ to_addresses=[to_email],
439
+ subject=subject,
440
+ body_text=message,
441
+ body_html=html_message
442
+ )
443
+
444
+
445
+ def verify_sender(email: str) -> bool:
446
+ """
447
+ Verify an email address for sending.
448
+
449
+ Args:
450
+ email: Email address to verify
451
+
452
+ Returns:
453
+ True if verification process started successfully
454
+ """
455
+ sender = EmailSender()
456
+ return sender.verify_email_identity(email)
457
+
458
+
459
+ def is_identity_verified(identity: str) -> bool:
460
+ """
461
+ Check if an identity is verified.
462
+
463
+ Args:
464
+ identity: Email address or domain to check
465
+
466
+ Returns:
467
+ True if verified, False otherwise
468
+ """
469
+ client = boto3.client('ses',
470
+ aws_access_key_id=settings.AWS_KEY,
471
+ aws_secret_access_key=settings.AWS_SECRET,
472
+ region_name=getattr(settings, 'AWS_REGION', 'us-east-1'))
473
+
474
+ try:
475
+ response = client.get_identity_verification_attributes(
476
+ Identities=[identity]
477
+ )
478
+ attributes = response.get('VerificationAttributes', {}).get(identity, {})
479
+ status = attributes.get('VerificationStatus', '')
480
+ return status.lower() == 'success'
481
+ except botocore.exceptions.ClientError as e:
482
+ logger.error(f"Failed to check verification status for {identity}: {e}")
483
+ return False