django-nativemojo 0.1.10__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 (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
@@ -0,0 +1,30 @@
1
+ from django.db import models
2
+ from django.template import Context
3
+ from django.template import Template
4
+ from mojo.models import MojoModel
5
+
6
+
7
+ try:
8
+ import css_inline
9
+ inliner = css_inline.CSSInliner(remove_style_tags=True)
10
+ except Exception:
11
+ inliner = None
12
+
13
+
14
+ class NotifyTemplate(models.Model, MojoModel):
15
+ created = models.DateTimeField(auto_now_add=True, editable=False)
16
+ modified = models.DateTimeField(auto_now=True)
17
+ group = models.ForeignKey("account.Group", blank=True, null=True, default=None,
18
+ related_name="templates", on_delete=models.CASCADE)
19
+ name = models.CharField(max_length=255, db_index=True)
20
+ kind = models.CharField(max_length=124, default="email")
21
+ template = models.TextField()
22
+
23
+ def render(self, context):
24
+ template = Template(self.template)
25
+ if context is None:
26
+ context = {}
27
+ context = Context(context)
28
+ if inliner is not None:
29
+ return inliner.inline(template.render(context))
30
+ return template.render(context)
File without changes
@@ -0,0 +1,73 @@
1
+ from mojo.helpers.settings import settings
2
+ from mojo.helpers.logit import get_logger
3
+ import metrics
4
+ import boto3
5
+ from . import render as mr
6
+
7
+ # Initialize logger for email notifications
8
+ EMAIL_LOGGER = get_logger("email", filename="email.log")
9
+
10
+ # Retrieve SES settings from configuration
11
+ SES_ACCESS_KEY = settings.SES_ACCESS_KEY
12
+ SES_SECRET_KEY = settings.SES_SECRET_KEY
13
+ SES_REGION = settings.SES_REGION
14
+ EMAIL_METRICS = settings.EMAIL_METRICS
15
+ EMAIL_ASYNC_AS_TASK = settings.EMAIL_ASYNC_AS_TASK
16
+
17
+ def get_ses_client(access_key, secret_key, region):
18
+ """Create a new SES client with the provided credentials."""
19
+ return boto3.client('ses',
20
+ aws_access_key_id=access_key,
21
+ aws_secret_access_key=secret_key,
22
+ region_name=region)
23
+
24
+ def send_mail_via_ses(msg, sender, recipients, fail_silently=True):
25
+ """Send an email using Amazon SES service."""
26
+ try:
27
+ ses_client = get_ses_client(SES_ACCESS_KEY, SES_SECRET_KEY, SES_REGION)
28
+ ses_client.send_raw_email(
29
+ Source=sender,
30
+ Destinations=recipients,
31
+ RawMessage={'Data': msg.as_string()}
32
+ )
33
+ if EMAIL_METRICS:
34
+ metrics.record("emails_sent", category="email", min_granularity="hourly")
35
+ return True
36
+ except Exception as err:
37
+ if EMAIL_METRICS:
38
+ metrics.record("email_errors", category="email", min_granularity="hourly")
39
+ EMAIL_LOGGER.exception(err)
40
+ EMAIL_LOGGER.error(msg.as_string())
41
+ if not fail_silently:
42
+ raise err
43
+ return False
44
+
45
+ def send_mail(msg, sender, recipients, fail_silently=True):
46
+ """Send an email via SES, defaulting to fail silently."""
47
+ return send_mail_via_ses(msg, sender, recipients, fail_silently)
48
+
49
+ def send(sender, recipients, subject, message, attachments=None, replyto=None, fail_silently=False, do_async=False):
50
+ """
51
+ Prepare and send an email message.
52
+
53
+ :param sender: Email address of the sender.
54
+ :param recipients: List of recipient email addresses.
55
+ :param subject: Subject of the email.
56
+ :param message: Body of the email.
57
+ :param attachments: List of attachments.
58
+ :param replyto: Email address to reply to.
59
+ :param fail_silently: Flag to suppress exceptions.
60
+ :param do_async: Flag to send email asynchronously (not implemented).
61
+ """
62
+ html = None
63
+ text = None
64
+
65
+ if mr.isHTML(message):
66
+ html = message
67
+ else:
68
+ text = message
69
+
70
+ msg = mr.createMessage(sender, recipients, subject, text, html,
71
+ attachments=attachments, replyto=replyto)
72
+
73
+ return send_mail(msg.msg, msg.sender, msg.recipients, fail_silently=fail_silently)
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ from .render import render_template # noqa: F401
2
+ from .parsing import * # noqa: F401
@@ -0,0 +1,404 @@
1
+ from django.utils import timezone
2
+ from django.db import transaction
3
+ from django.core.exceptions import ValidationError
4
+ from typing import Optional, Dict, Any, List, Union
5
+ import logging
6
+
7
+ from ..models import Account, Inbox, InboxMessage, Outbox, OutboxMessage
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class NotificationError(Exception):
14
+ """Base exception for notification errors"""
15
+ pass
16
+
17
+
18
+ class MessageSender:
19
+ """
20
+ High-level interface for sending messages through various notification channels
21
+ """
22
+
23
+ @staticmethod
24
+ def send_email(
25
+ to_address: str,
26
+ subject: str,
27
+ message: str,
28
+ from_address: Optional[str] = None,
29
+ group=None,
30
+ user=None,
31
+ metadata: Optional[Dict[str, Any]] = None,
32
+ scheduled_at: Optional[timezone.datetime] = None
33
+ ) -> OutboxMessage:
34
+ """Send an email message"""
35
+ return MessageSender._send_message(
36
+ kind=Account.EMAIL,
37
+ to_address=to_address,
38
+ subject=subject,
39
+ message=message,
40
+ from_address=from_address,
41
+ group=group,
42
+ user=user,
43
+ metadata=metadata or {},
44
+ scheduled_at=scheduled_at
45
+ )
46
+
47
+ @staticmethod
48
+ def send_sms(
49
+ to_address: str,
50
+ message: str,
51
+ from_address: Optional[str] = None,
52
+ group=None,
53
+ user=None,
54
+ metadata: Optional[Dict[str, Any]] = None,
55
+ scheduled_at: Optional[timezone.datetime] = None
56
+ ) -> OutboxMessage:
57
+ """Send an SMS message"""
58
+ return MessageSender._send_message(
59
+ kind=Account.SMS,
60
+ to_address=to_address,
61
+ subject=None,
62
+ message=message,
63
+ from_address=from_address,
64
+ group=group,
65
+ user=user,
66
+ metadata=metadata or {},
67
+ scheduled_at=scheduled_at
68
+ )
69
+
70
+ @staticmethod
71
+ def send_whatsapp(
72
+ to_address: str,
73
+ message: str,
74
+ from_address: Optional[str] = None,
75
+ group=None,
76
+ user=None,
77
+ metadata: Optional[Dict[str, Any]] = None,
78
+ scheduled_at: Optional[timezone.datetime] = None
79
+ ) -> OutboxMessage:
80
+ """Send a WhatsApp message"""
81
+ return MessageSender._send_message(
82
+ kind=Account.WHATSAPP,
83
+ to_address=to_address,
84
+ subject=None,
85
+ message=message,
86
+ from_address=from_address,
87
+ group=group,
88
+ user=user,
89
+ metadata=metadata or {},
90
+ scheduled_at=scheduled_at
91
+ )
92
+
93
+ @staticmethod
94
+ def send_push(
95
+ to_address: str,
96
+ message: str,
97
+ title: Optional[str] = None,
98
+ from_address: Optional[str] = None,
99
+ group=None,
100
+ user=None,
101
+ metadata: Optional[Dict[str, Any]] = None,
102
+ scheduled_at: Optional[timezone.datetime] = None
103
+ ) -> OutboxMessage:
104
+ """Send a push notification"""
105
+ metadata = metadata or {}
106
+ if title:
107
+ metadata['title'] = title
108
+
109
+ return MessageSender._send_message(
110
+ kind=Account.PUSH,
111
+ to_address=to_address,
112
+ subject=title,
113
+ message=message,
114
+ from_address=from_address,
115
+ group=group,
116
+ user=user,
117
+ metadata=metadata,
118
+ scheduled_at=scheduled_at
119
+ )
120
+
121
+ @staticmethod
122
+ def _send_message(
123
+ kind: str,
124
+ to_address: str,
125
+ message: str,
126
+ subject: Optional[str] = None,
127
+ from_address: Optional[str] = None,
128
+ group=None,
129
+ user=None,
130
+ metadata: Optional[Dict[str, Any]] = None,
131
+ scheduled_at: Optional[timezone.datetime] = None
132
+ ) -> OutboxMessage:
133
+ """Internal method to send messages of any kind"""
134
+
135
+ # Find appropriate outbox
136
+ outbox = OutboxFinder.find_outbox(
137
+ kind=kind,
138
+ from_address=from_address,
139
+ group=group
140
+ )
141
+
142
+ if not outbox:
143
+ raise NotificationError(f"No active outbox found for {kind} messages")
144
+
145
+ if not outbox.can_send_messages():
146
+ raise NotificationError(f"Outbox {outbox} cannot send messages")
147
+
148
+ if not outbox.check_rate_limit():
149
+ raise NotificationError(f"Outbox {outbox} has exceeded rate limit")
150
+
151
+ # Use outbox address if no from_address specified
152
+ if not from_address:
153
+ from_address = outbox.address
154
+
155
+ # Create the outbox message
156
+ with transaction.atomic():
157
+ outbox_message = OutboxMessage.objects.create(
158
+ outbox=outbox,
159
+ user=user,
160
+ group=group or outbox.group,
161
+ to_address=to_address,
162
+ from_address=from_address,
163
+ subject=subject,
164
+ message=message,
165
+ metadata=metadata or {},
166
+ scheduled_at=scheduled_at
167
+ )
168
+
169
+ logger.info(f"Queued {kind} message from {from_address} to {to_address}")
170
+ return outbox_message
171
+
172
+
173
+ class OutboxFinder:
174
+ """
175
+ Utility for finding appropriate outboxes for sending messages
176
+ """
177
+
178
+ @staticmethod
179
+ def find_outbox(
180
+ kind: str,
181
+ from_address: Optional[str] = None,
182
+ group=None
183
+ ) -> Optional[Outbox]:
184
+ """Find the best outbox for sending a message"""
185
+
186
+ query = Outbox.objects.filter(
187
+ account__kind=kind,
188
+ account__is_active=True,
189
+ is_active=True
190
+ ).select_related('account', 'group')
191
+
192
+ # Prefer outboxes with matching group
193
+ if group:
194
+ group_matches = query.filter(group=group)
195
+ if group_matches.exists():
196
+ query = group_matches
197
+
198
+ # Prefer outboxes with matching address
199
+ if from_address:
200
+ address_matches = query.filter(address=from_address)
201
+ if address_matches.exists():
202
+ return address_matches.first()
203
+
204
+ # Return any available outbox
205
+ return query.first()
206
+
207
+ @staticmethod
208
+ def get_outboxes_for_account(account: Account) -> List[Outbox]:
209
+ """Get all active outboxes for an account"""
210
+ return list(
211
+ Outbox.objects.filter(
212
+ account=account,
213
+ is_active=True
214
+ ).select_related('group')
215
+ )
216
+
217
+
218
+ class MessageProcessor:
219
+ """
220
+ Utility for processing received messages
221
+ """
222
+
223
+ @staticmethod
224
+ def process_inbox_message(inbox_message: InboxMessage) -> bool:
225
+ """Process a received inbox message"""
226
+ if inbox_message.processed:
227
+ return True
228
+
229
+ inbox = inbox_message.inbox
230
+ handler_path = inbox.get_handler()
231
+
232
+ if not handler_path:
233
+ logger.warning(f"No handler configured for inbox {inbox}")
234
+ return False
235
+
236
+ try:
237
+ # Import and call the handler
238
+ handler_func = _import_handler(handler_path)
239
+ if handler_func:
240
+ result = handler_func(inbox_message)
241
+ inbox_message.mark_processed()
242
+ logger.info(f"Processed message {inbox_message.id} with handler {handler_path}")
243
+ return True
244
+ except Exception as e:
245
+ logger.error(f"Error processing message {inbox_message.id}: {e}")
246
+ return False
247
+
248
+ return False
249
+
250
+ @staticmethod
251
+ def bulk_process_inbox_messages(inbox: Inbox, limit: int = 100) -> int:
252
+ """Process multiple unprocessed messages for an inbox"""
253
+ messages = InboxMessage.objects.filter(
254
+ inbox=inbox,
255
+ processed=False
256
+ ).order_by('created')[:limit]
257
+
258
+ processed_count = 0
259
+ for message in messages:
260
+ if MessageProcessor.process_inbox_message(message):
261
+ processed_count += 1
262
+
263
+ return processed_count
264
+
265
+
266
+ class BulkNotifier:
267
+ """
268
+ Utility for sending bulk notifications
269
+ """
270
+
271
+ @staticmethod
272
+ def send_bulk_email(
273
+ recipients: List[str],
274
+ subject: str,
275
+ message: str,
276
+ from_address: Optional[str] = None,
277
+ group=None,
278
+ metadata: Optional[Dict[str, Any]] = None,
279
+ scheduled_at: Optional[timezone.datetime] = None,
280
+ batch_size: int = 100
281
+ ) -> List[OutboxMessage]:
282
+ """Send bulk email messages"""
283
+
284
+ messages = []
285
+ for i in range(0, len(recipients), batch_size):
286
+ batch = recipients[i:i + batch_size]
287
+
288
+ with transaction.atomic():
289
+ for recipient in batch:
290
+ try:
291
+ msg = MessageSender.send_email(
292
+ to_address=recipient,
293
+ subject=subject,
294
+ message=message,
295
+ from_address=from_address,
296
+ group=group,
297
+ metadata=metadata,
298
+ scheduled_at=scheduled_at
299
+ )
300
+ messages.append(msg)
301
+ except Exception as e:
302
+ logger.error(f"Failed to queue email to {recipient}: {e}")
303
+
304
+ return messages
305
+
306
+ @staticmethod
307
+ def send_bulk_sms(
308
+ recipients: List[str],
309
+ message: str,
310
+ from_address: Optional[str] = None,
311
+ group=None,
312
+ metadata: Optional[Dict[str, Any]] = None,
313
+ scheduled_at: Optional[timezone.datetime] = None,
314
+ batch_size: int = 100
315
+ ) -> List[OutboxMessage]:
316
+ """Send bulk SMS messages"""
317
+
318
+ messages = []
319
+ for i in range(0, len(recipients), batch_size):
320
+ batch = recipients[i:i + batch_size]
321
+
322
+ with transaction.atomic():
323
+ for recipient in batch:
324
+ try:
325
+ msg = MessageSender.send_sms(
326
+ to_address=recipient,
327
+ message=message,
328
+ from_address=from_address,
329
+ group=group,
330
+ metadata=metadata,
331
+ scheduled_at=scheduled_at
332
+ )
333
+ messages.append(msg)
334
+ except Exception as e:
335
+ logger.error(f"Failed to queue SMS to {recipient}: {e}")
336
+
337
+ return messages
338
+
339
+
340
+ class MessageStats:
341
+ """
342
+ Utility for getting message statistics
343
+ """
344
+
345
+ @staticmethod
346
+ def get_outbox_stats(outbox: Outbox) -> Dict[str, Any]:
347
+ """Get statistics for an outbox"""
348
+ messages = outbox.messages.all()
349
+
350
+ return {
351
+ 'total_messages': messages.count(),
352
+ 'pending_messages': messages.filter(status=OutboxMessage.PENDING).count(),
353
+ 'sent_messages': messages.filter(status=OutboxMessage.SENT).count(),
354
+ 'failed_messages': messages.filter(status=OutboxMessage.FAILED).count(),
355
+ 'recent_messages': messages.filter(
356
+ created__gte=timezone.now() - timezone.timedelta(hours=24)
357
+ ).count(),
358
+ }
359
+
360
+ @staticmethod
361
+ def get_inbox_stats(inbox: Inbox) -> Dict[str, Any]:
362
+ """Get statistics for an inbox"""
363
+ messages = inbox.messages.all()
364
+
365
+ return {
366
+ 'total_messages': messages.count(),
367
+ 'processed_messages': messages.filter(processed=True).count(),
368
+ 'unprocessed_messages': messages.filter(processed=False).count(),
369
+ 'recent_messages': messages.filter(
370
+ created__gte=timezone.now() - timezone.timedelta(hours=24)
371
+ ).count(),
372
+ }
373
+
374
+
375
+ def _import_handler(handler_path: str):
376
+ """Import a handler function from a module path"""
377
+ try:
378
+ module_path, function_name = handler_path.rsplit('.', 1)
379
+ module = __import__(module_path, fromlist=[function_name])
380
+ return getattr(module, function_name)
381
+ except (ImportError, AttributeError, ValueError) as e:
382
+ logger.error(f"Failed to import handler {handler_path}: {e}")
383
+ return None
384
+
385
+
386
+ # Convenience functions for common operations
387
+ def send_email(to_address: str, subject: str, message: str, **kwargs) -> OutboxMessage:
388
+ """Convenience function to send an email"""
389
+ return MessageSender.send_email(to_address, subject, message, **kwargs)
390
+
391
+
392
+ def send_sms(to_address: str, message: str, **kwargs) -> OutboxMessage:
393
+ """Convenience function to send an SMS"""
394
+ return MessageSender.send_sms(to_address, message, **kwargs)
395
+
396
+
397
+ def send_whatsapp(to_address: str, message: str, **kwargs) -> OutboxMessage:
398
+ """Convenience function to send a WhatsApp message"""
399
+ return MessageSender.send_whatsapp(to_address, message, **kwargs)
400
+
401
+
402
+ def send_push(to_address: str, message: str, title: Optional[str] = None, **kwargs) -> OutboxMessage:
403
+ """Convenience function to send a push notification"""
404
+ return MessageSender.send_push(to_address, message, title, **kwargs)