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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
mojo/helpers/aws/ses.py
ADDED
@@ -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
|