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.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class Inbox(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
Inbox for receiving messages from various notification services
|
8
|
+
"""
|
9
|
+
|
10
|
+
class RestMeta:
|
11
|
+
CAN_SAVE = CAN_CREATE = True
|
12
|
+
CAN_DELETE = True
|
13
|
+
DEFAULT_SORT = "-id"
|
14
|
+
VIEW_PERMS = ["view_notify"]
|
15
|
+
SEARCH_FIELDS = ["address"]
|
16
|
+
SEARCH_TERMS = [
|
17
|
+
"address",
|
18
|
+
("account", "account__domain"),
|
19
|
+
("account_kind", "account__kind"),
|
20
|
+
("group", "account__group__name")]
|
21
|
+
|
22
|
+
GRAPHS = {
|
23
|
+
"default": {
|
24
|
+
"graphs": {
|
25
|
+
"account": "basic"
|
26
|
+
}
|
27
|
+
},
|
28
|
+
"list": {
|
29
|
+
"graphs": {
|
30
|
+
"account": "basic"
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
36
|
+
modified = models.DateTimeField(auto_now=True)
|
37
|
+
|
38
|
+
account = models.ForeignKey(
|
39
|
+
"notify.Account",
|
40
|
+
related_name="inboxes",
|
41
|
+
on_delete=models.CASCADE,
|
42
|
+
help_text="Notification account this inbox belongs to"
|
43
|
+
)
|
44
|
+
|
45
|
+
address = models.CharField(
|
46
|
+
max_length=255,
|
47
|
+
db_index=True,
|
48
|
+
help_text="Inbox address (e.g., support@example.com, +1234567890)"
|
49
|
+
)
|
50
|
+
|
51
|
+
async_handler = models.CharField(
|
52
|
+
max_length=255,
|
53
|
+
null=True,
|
54
|
+
blank=True,
|
55
|
+
default=None,
|
56
|
+
help_text="Python path to async handler function (e.g., app.inbox_handler.on_message_received)"
|
57
|
+
)
|
58
|
+
|
59
|
+
sync_handler = models.CharField(
|
60
|
+
max_length=255,
|
61
|
+
null=True,
|
62
|
+
blank=True,
|
63
|
+
default=None,
|
64
|
+
help_text="Python path to sync handler function (e.g., app.inbox_handler.on_message_received)"
|
65
|
+
)
|
66
|
+
|
67
|
+
is_active = models.BooleanField(
|
68
|
+
default=True,
|
69
|
+
help_text="Whether this inbox is active and can receive messages"
|
70
|
+
)
|
71
|
+
|
72
|
+
settings = models.JSONField(
|
73
|
+
default=dict,
|
74
|
+
blank=True,
|
75
|
+
help_text="Inbox-specific configuration settings"
|
76
|
+
)
|
77
|
+
|
78
|
+
class Meta:
|
79
|
+
unique_together = ['account', 'address']
|
80
|
+
indexes = [
|
81
|
+
models.Index(fields=['account', 'address']),
|
82
|
+
models.Index(fields=['address', 'is_active']),
|
83
|
+
]
|
84
|
+
|
85
|
+
def __str__(self):
|
86
|
+
return f"{self.account.get_kind_display()} inbox: {self.address}"
|
87
|
+
|
88
|
+
def get_setting(self, key, default=None):
|
89
|
+
"""Get a specific setting value"""
|
90
|
+
return self.settings.get(key, default)
|
91
|
+
|
92
|
+
def set_setting(self, key, value):
|
93
|
+
"""Set a specific setting value"""
|
94
|
+
self.settings[key] = value
|
95
|
+
|
96
|
+
@property
|
97
|
+
def group(self):
|
98
|
+
"""Get the group this inbox belongs to through its account"""
|
99
|
+
return self.account.group if self.account else None
|
100
|
+
|
101
|
+
def can_receive_messages(self):
|
102
|
+
"""Check if this inbox can receive messages"""
|
103
|
+
return self.is_active and self.account.is_active
|
104
|
+
|
105
|
+
def get_handler(self, async_preferred=True):
|
106
|
+
"""Get the appropriate handler for message processing"""
|
107
|
+
if async_preferred and self.async_handler:
|
108
|
+
return self.async_handler
|
109
|
+
elif self.sync_handler:
|
110
|
+
return self.sync_handler
|
111
|
+
elif self.async_handler:
|
112
|
+
return self.async_handler
|
113
|
+
return None
|
@@ -0,0 +1,173 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class InboxMessage(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
Message received in an inbox from various notification services
|
8
|
+
"""
|
9
|
+
|
10
|
+
class RestMeta:
|
11
|
+
CAN_SAVE = CAN_CREATE = True
|
12
|
+
CAN_DELETE = True
|
13
|
+
DEFAULT_SORT = "-id"
|
14
|
+
VIEW_PERMS = ["view_notify"]
|
15
|
+
SEARCH_FIELDS = ["to_address", "from_address", "subject", "message"]
|
16
|
+
SEARCH_TERMS = [
|
17
|
+
"to_address", "from_address", "subject",
|
18
|
+
("inbox", "inbox__address"),
|
19
|
+
("account", "inbox__account__domain"),
|
20
|
+
("account_kind", "inbox__account__kind"),
|
21
|
+
("user", "user__username"),
|
22
|
+
("group", "group__name")]
|
23
|
+
|
24
|
+
GRAPHS = {
|
25
|
+
"default": {
|
26
|
+
"graphs": {
|
27
|
+
"inbox": "basic",
|
28
|
+
"user": "basic",
|
29
|
+
"group": "basic"
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"list": {
|
33
|
+
"graphs": {
|
34
|
+
"inbox": "basic",
|
35
|
+
"user": "basic",
|
36
|
+
"group": "basic"
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
42
|
+
modified = models.DateTimeField(auto_now=True)
|
43
|
+
|
44
|
+
inbox = models.ForeignKey(
|
45
|
+
"notify.Inbox",
|
46
|
+
related_name="messages",
|
47
|
+
on_delete=models.CASCADE,
|
48
|
+
help_text="Inbox that received this message"
|
49
|
+
)
|
50
|
+
|
51
|
+
user = models.ForeignKey(
|
52
|
+
"account.User",
|
53
|
+
related_name="inbox_messages",
|
54
|
+
null=True,
|
55
|
+
blank=True,
|
56
|
+
default=None,
|
57
|
+
on_delete=models.CASCADE,
|
58
|
+
help_text="User associated with this message (if applicable)"
|
59
|
+
)
|
60
|
+
|
61
|
+
group = models.ForeignKey(
|
62
|
+
"account.Group",
|
63
|
+
related_name="inbox_messages",
|
64
|
+
null=True,
|
65
|
+
blank=True,
|
66
|
+
default=None,
|
67
|
+
on_delete=models.CASCADE,
|
68
|
+
help_text="Group associated with this message (if applicable)"
|
69
|
+
)
|
70
|
+
|
71
|
+
to_address = models.CharField(
|
72
|
+
max_length=255,
|
73
|
+
db_index=True,
|
74
|
+
help_text="Recipient address (e.g., inbox@example.com, +1234567890)"
|
75
|
+
)
|
76
|
+
|
77
|
+
from_address = models.CharField(
|
78
|
+
max_length=255,
|
79
|
+
db_index=True,
|
80
|
+
help_text="Sender address (e.g., sender@example.com, +0987654321)"
|
81
|
+
)
|
82
|
+
|
83
|
+
subject = models.CharField(
|
84
|
+
max_length=500,
|
85
|
+
null=True,
|
86
|
+
blank=True,
|
87
|
+
default=None,
|
88
|
+
help_text="Message subject (for email, etc.)"
|
89
|
+
)
|
90
|
+
|
91
|
+
message = models.TextField(
|
92
|
+
help_text="Message content/body"
|
93
|
+
)
|
94
|
+
|
95
|
+
metadata = models.JSONField(
|
96
|
+
default=dict,
|
97
|
+
blank=True,
|
98
|
+
help_text="Additional message metadata (headers, delivery info, etc.)"
|
99
|
+
)
|
100
|
+
|
101
|
+
processed = models.BooleanField(
|
102
|
+
default=False,
|
103
|
+
db_index=True,
|
104
|
+
help_text="Whether this message has been processed by handlers"
|
105
|
+
)
|
106
|
+
|
107
|
+
processed_at = models.DateTimeField(
|
108
|
+
null=True,
|
109
|
+
blank=True,
|
110
|
+
default=None,
|
111
|
+
help_text="When this message was processed"
|
112
|
+
)
|
113
|
+
|
114
|
+
message_id = models.CharField(
|
115
|
+
max_length=255,
|
116
|
+
null=True,
|
117
|
+
blank=True,
|
118
|
+
default=None,
|
119
|
+
db_index=True,
|
120
|
+
help_text="External message ID from the service provider"
|
121
|
+
)
|
122
|
+
|
123
|
+
class Meta:
|
124
|
+
indexes = [
|
125
|
+
models.Index(fields=['inbox', 'created']),
|
126
|
+
models.Index(fields=['from_address', 'created']),
|
127
|
+
models.Index(fields=['to_address', 'created']),
|
128
|
+
models.Index(fields=['processed', 'created']),
|
129
|
+
models.Index(fields=['user', 'created']),
|
130
|
+
models.Index(fields=['group', 'created']),
|
131
|
+
]
|
132
|
+
|
133
|
+
def __str__(self):
|
134
|
+
subject_preview = f" - {self.subject[:50]}..." if self.subject else ""
|
135
|
+
return f"{self.inbox.account.get_kind_display()} message: {self.from_address} → {self.to_address}{subject_preview}"
|
136
|
+
|
137
|
+
def get_metadata_value(self, key, default=None):
|
138
|
+
"""Get a specific metadata value"""
|
139
|
+
return self.metadata.get(key, default)
|
140
|
+
|
141
|
+
def set_metadata_value(self, key, value):
|
142
|
+
"""Set a specific metadata value"""
|
143
|
+
self.metadata[key] = value
|
144
|
+
|
145
|
+
@property
|
146
|
+
def account(self):
|
147
|
+
"""Get the account this message was received through"""
|
148
|
+
return self.inbox.account if self.inbox else None
|
149
|
+
|
150
|
+
@property
|
151
|
+
def message_kind(self):
|
152
|
+
"""Get the kind of message (email, sms, etc.)"""
|
153
|
+
return self.account.kind if self.account else None
|
154
|
+
|
155
|
+
@property
|
156
|
+
def message_preview(self):
|
157
|
+
"""Get a preview of the message content"""
|
158
|
+
if not self.message:
|
159
|
+
return ""
|
160
|
+
return self.message[:200] + "..." if len(self.message) > 200 else self.message
|
161
|
+
|
162
|
+
def mark_processed(self):
|
163
|
+
"""Mark this message as processed"""
|
164
|
+
from django.utils import timezone
|
165
|
+
self.processed = True
|
166
|
+
self.processed_at = timezone.now()
|
167
|
+
self.save(update_fields=['processed', 'processed_at'])
|
168
|
+
|
169
|
+
def is_from_user(self, user):
|
170
|
+
"""Check if this message is from a specific user"""
|
171
|
+
if not user or not user.email:
|
172
|
+
return False
|
173
|
+
return self.from_address.lower() == user.email.lower()
|
@@ -0,0 +1,129 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class Outbox(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
Outbox for sending messages through various notification services
|
8
|
+
"""
|
9
|
+
|
10
|
+
class RestMeta:
|
11
|
+
CAN_SAVE = CAN_CREATE = True
|
12
|
+
CAN_DELETE = True
|
13
|
+
DEFAULT_SORT = "-id"
|
14
|
+
VIEW_PERMS = ["view_notify"]
|
15
|
+
SEARCH_FIELDS = ["address"]
|
16
|
+
SEARCH_TERMS = [
|
17
|
+
"address",
|
18
|
+
("account", "account__domain"),
|
19
|
+
("account_kind", "account__kind"),
|
20
|
+
("group", "group__name")]
|
21
|
+
|
22
|
+
GRAPHS = {
|
23
|
+
"default": {
|
24
|
+
"graphs": {
|
25
|
+
"account": "basic",
|
26
|
+
"group": "basic"
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"list": {
|
30
|
+
"graphs": {
|
31
|
+
"account": "basic",
|
32
|
+
"group": "basic"
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
38
|
+
modified = models.DateTimeField(auto_now=True)
|
39
|
+
|
40
|
+
account = models.ForeignKey(
|
41
|
+
"notify.Account",
|
42
|
+
related_name="outboxes",
|
43
|
+
on_delete=models.CASCADE,
|
44
|
+
help_text="Notification account this outbox belongs to"
|
45
|
+
)
|
46
|
+
|
47
|
+
group = models.ForeignKey(
|
48
|
+
"account.Group",
|
49
|
+
related_name="outboxes",
|
50
|
+
null=True,
|
51
|
+
blank=True,
|
52
|
+
default=None,
|
53
|
+
on_delete=models.CASCADE,
|
54
|
+
help_text="Group that owns this outbox"
|
55
|
+
)
|
56
|
+
|
57
|
+
address = models.CharField(
|
58
|
+
max_length=255,
|
59
|
+
db_index=True,
|
60
|
+
help_text="Outbox address (e.g., outbox@example.com, +1234567890)"
|
61
|
+
)
|
62
|
+
|
63
|
+
handler = models.CharField(
|
64
|
+
max_length=255,
|
65
|
+
null=True,
|
66
|
+
blank=True,
|
67
|
+
default=None,
|
68
|
+
help_text="Python path to handler function (e.g., app.outbox_handler.on_message_sent)"
|
69
|
+
)
|
70
|
+
|
71
|
+
is_active = models.BooleanField(
|
72
|
+
default=True,
|
73
|
+
help_text="Whether this outbox is active and can send messages"
|
74
|
+
)
|
75
|
+
|
76
|
+
settings = models.JSONField(
|
77
|
+
default=dict,
|
78
|
+
blank=True,
|
79
|
+
help_text="Outbox-specific configuration settings"
|
80
|
+
)
|
81
|
+
|
82
|
+
rate_limit = models.IntegerField(
|
83
|
+
null=True,
|
84
|
+
blank=True,
|
85
|
+
default=None,
|
86
|
+
help_text="Max messages per hour (null for no limit)"
|
87
|
+
)
|
88
|
+
|
89
|
+
class Meta:
|
90
|
+
unique_together = ['account', 'address']
|
91
|
+
indexes = [
|
92
|
+
models.Index(fields=['account', 'address']),
|
93
|
+
models.Index(fields=['group', 'is_active']),
|
94
|
+
models.Index(fields=['address', 'is_active']),
|
95
|
+
]
|
96
|
+
|
97
|
+
def __str__(self):
|
98
|
+
group_name = self.group.name if self.group else "No Group"
|
99
|
+
return f"{self.account.get_kind_display()} outbox: {self.address} ({group_name})"
|
100
|
+
|
101
|
+
def get_setting(self, key, default=None):
|
102
|
+
"""Get a specific setting value"""
|
103
|
+
return self.settings.get(key, default)
|
104
|
+
|
105
|
+
def set_setting(self, key, value):
|
106
|
+
"""Set a specific setting value"""
|
107
|
+
self.settings[key] = value
|
108
|
+
|
109
|
+
def can_send_messages(self):
|
110
|
+
"""Check if this outbox can send messages"""
|
111
|
+
return self.is_active and self.account.is_active
|
112
|
+
|
113
|
+
def check_rate_limit(self):
|
114
|
+
"""Check if outbox is within rate limits"""
|
115
|
+
if not self.rate_limit:
|
116
|
+
return True
|
117
|
+
|
118
|
+
from django.utils import timezone
|
119
|
+
from datetime import timedelta
|
120
|
+
|
121
|
+
one_hour_ago = timezone.now() - timedelta(hours=1)
|
122
|
+
recent_messages = self.messages.filter(created__gte=one_hour_ago).count()
|
123
|
+
|
124
|
+
return recent_messages < self.rate_limit
|
125
|
+
|
126
|
+
@property
|
127
|
+
def message_kind(self):
|
128
|
+
"""Get the kind of messages this outbox sends"""
|
129
|
+
return self.account.kind if self.account else None
|
@@ -0,0 +1,288 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class OutboxMessage(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
Message to be sent or already sent through an outbox
|
8
|
+
"""
|
9
|
+
|
10
|
+
class RestMeta:
|
11
|
+
CAN_SAVE = CAN_CREATE = True
|
12
|
+
CAN_DELETE = True
|
13
|
+
DEFAULT_SORT = "-id"
|
14
|
+
VIEW_PERMS = ["view_notify"]
|
15
|
+
SEARCH_FIELDS = ["to_address", "from_address", "subject", "message"]
|
16
|
+
SEARCH_TERMS = [
|
17
|
+
"to_address", "from_address", "subject", "status",
|
18
|
+
("outbox", "outbox__address"),
|
19
|
+
("account", "outbox__account__domain"),
|
20
|
+
("account_kind", "outbox__account__kind"),
|
21
|
+
("user", "user__username"),
|
22
|
+
("group", "group__name")]
|
23
|
+
|
24
|
+
GRAPHS = {
|
25
|
+
"default": {
|
26
|
+
"graphs": {
|
27
|
+
"outbox": "basic",
|
28
|
+
"user": "basic",
|
29
|
+
"group": "basic"
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"list": {
|
33
|
+
"graphs": {
|
34
|
+
"outbox": "basic",
|
35
|
+
"user": "basic",
|
36
|
+
"group": "basic"
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
# Message status choices
|
42
|
+
PENDING = 'pending'
|
43
|
+
SENDING = 'sending'
|
44
|
+
SENT = 'sent'
|
45
|
+
FAILED = 'failed'
|
46
|
+
CANCELLED = 'cancelled'
|
47
|
+
|
48
|
+
STATUS_CHOICES = [
|
49
|
+
(PENDING, 'Pending'),
|
50
|
+
(SENDING, 'Sending'),
|
51
|
+
(SENT, 'Sent'),
|
52
|
+
(FAILED, 'Failed'),
|
53
|
+
(CANCELLED, 'Cancelled'),
|
54
|
+
]
|
55
|
+
|
56
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
57
|
+
modified = models.DateTimeField(auto_now=True)
|
58
|
+
|
59
|
+
outbox = models.ForeignKey(
|
60
|
+
"notify.Outbox",
|
61
|
+
related_name="messages",
|
62
|
+
on_delete=models.CASCADE,
|
63
|
+
help_text="Outbox that will send this message"
|
64
|
+
)
|
65
|
+
|
66
|
+
user = models.ForeignKey(
|
67
|
+
"account.User",
|
68
|
+
related_name="outbox_messages",
|
69
|
+
null=True,
|
70
|
+
blank=True,
|
71
|
+
default=None,
|
72
|
+
on_delete=models.CASCADE,
|
73
|
+
help_text="User associated with this message (if applicable)"
|
74
|
+
)
|
75
|
+
|
76
|
+
group = models.ForeignKey(
|
77
|
+
"account.Group",
|
78
|
+
related_name="outbox_messages",
|
79
|
+
null=True,
|
80
|
+
blank=True,
|
81
|
+
default=None,
|
82
|
+
on_delete=models.CASCADE,
|
83
|
+
help_text="Group associated with this message (if applicable)"
|
84
|
+
)
|
85
|
+
|
86
|
+
to_address = models.CharField(
|
87
|
+
max_length=255,
|
88
|
+
db_index=True,
|
89
|
+
help_text="Recipient address (e.g., joe@example.com, +1234567890)"
|
90
|
+
)
|
91
|
+
|
92
|
+
from_address = models.CharField(
|
93
|
+
max_length=255,
|
94
|
+
db_index=True,
|
95
|
+
help_text="Sender address (e.g., outbox@example.com, +0987654321)"
|
96
|
+
)
|
97
|
+
|
98
|
+
subject = models.CharField(
|
99
|
+
max_length=500,
|
100
|
+
null=True,
|
101
|
+
blank=True,
|
102
|
+
default=None,
|
103
|
+
help_text="Message subject (for email, etc.)"
|
104
|
+
)
|
105
|
+
|
106
|
+
message = models.TextField(
|
107
|
+
help_text="Message content/body"
|
108
|
+
)
|
109
|
+
|
110
|
+
metadata = models.JSONField(
|
111
|
+
default=dict,
|
112
|
+
blank=True,
|
113
|
+
help_text="Additional message metadata (delivery options, attachments, etc.)"
|
114
|
+
)
|
115
|
+
|
116
|
+
status = models.CharField(
|
117
|
+
max_length=32,
|
118
|
+
choices=STATUS_CHOICES,
|
119
|
+
default=PENDING,
|
120
|
+
db_index=True,
|
121
|
+
help_text="Current status of the message"
|
122
|
+
)
|
123
|
+
|
124
|
+
scheduled_at = models.DateTimeField(
|
125
|
+
null=True,
|
126
|
+
blank=True,
|
127
|
+
default=None,
|
128
|
+
db_index=True,
|
129
|
+
help_text="When this message should be sent (null for immediate)"
|
130
|
+
)
|
131
|
+
|
132
|
+
sent_at = models.DateTimeField(
|
133
|
+
null=True,
|
134
|
+
blank=True,
|
135
|
+
default=None,
|
136
|
+
help_text="When this message was actually sent"
|
137
|
+
)
|
138
|
+
|
139
|
+
failed_at = models.DateTimeField(
|
140
|
+
null=True,
|
141
|
+
blank=True,
|
142
|
+
default=None,
|
143
|
+
help_text="When this message failed to send"
|
144
|
+
)
|
145
|
+
|
146
|
+
error_message = models.TextField(
|
147
|
+
null=True,
|
148
|
+
blank=True,
|
149
|
+
default=None,
|
150
|
+
help_text="Error message if sending failed"
|
151
|
+
)
|
152
|
+
|
153
|
+
message_id = models.CharField(
|
154
|
+
max_length=255,
|
155
|
+
null=True,
|
156
|
+
blank=True,
|
157
|
+
default=None,
|
158
|
+
db_index=True,
|
159
|
+
help_text="External message ID from the service provider"
|
160
|
+
)
|
161
|
+
|
162
|
+
retry_count = models.IntegerField(
|
163
|
+
default=0,
|
164
|
+
help_text="Number of times sending has been attempted"
|
165
|
+
)
|
166
|
+
|
167
|
+
max_retries = models.IntegerField(
|
168
|
+
default=3,
|
169
|
+
help_text="Maximum number of retry attempts"
|
170
|
+
)
|
171
|
+
|
172
|
+
class Meta:
|
173
|
+
indexes = [
|
174
|
+
models.Index(fields=['outbox', 'status', 'created']),
|
175
|
+
models.Index(fields=['status', 'scheduled_at']),
|
176
|
+
models.Index(fields=['to_address', 'created']),
|
177
|
+
models.Index(fields=['from_address', 'created']),
|
178
|
+
models.Index(fields=['user', 'created']),
|
179
|
+
models.Index(fields=['group', 'created']),
|
180
|
+
models.Index(fields=['sent_at']),
|
181
|
+
]
|
182
|
+
|
183
|
+
def __str__(self):
|
184
|
+
subject_preview = f" - {self.subject[:50]}..." if self.subject else ""
|
185
|
+
return f"{self.outbox.account.get_kind_display()} message: {self.from_address} → {self.to_address}{subject_preview} ({self.get_status_display()})"
|
186
|
+
|
187
|
+
def get_metadata_value(self, key, default=None):
|
188
|
+
"""Get a specific metadata value"""
|
189
|
+
return self.metadata.get(key, default)
|
190
|
+
|
191
|
+
def set_metadata_value(self, key, value):
|
192
|
+
"""Set a specific metadata value"""
|
193
|
+
self.metadata[key] = value
|
194
|
+
|
195
|
+
@property
|
196
|
+
def account(self):
|
197
|
+
"""Get the account this message will be sent through"""
|
198
|
+
return self.outbox.account if self.outbox else None
|
199
|
+
|
200
|
+
@property
|
201
|
+
def message_kind(self):
|
202
|
+
"""Get the kind of message (email, sms, etc.)"""
|
203
|
+
return self.account.kind if self.account else None
|
204
|
+
|
205
|
+
@property
|
206
|
+
def message_preview(self):
|
207
|
+
"""Get a preview of the message content"""
|
208
|
+
if not self.message:
|
209
|
+
return ""
|
210
|
+
return self.message[:200] + "..." if len(self.message) > 200 else self.message
|
211
|
+
|
212
|
+
@property
|
213
|
+
def is_pending(self):
|
214
|
+
return self.status == self.PENDING
|
215
|
+
|
216
|
+
@property
|
217
|
+
def is_sending(self):
|
218
|
+
return self.status == self.SENDING
|
219
|
+
|
220
|
+
@property
|
221
|
+
def is_sent(self):
|
222
|
+
return self.status == self.SENT
|
223
|
+
|
224
|
+
@property
|
225
|
+
def is_failed(self):
|
226
|
+
return self.status == self.FAILED
|
227
|
+
|
228
|
+
@property
|
229
|
+
def is_cancelled(self):
|
230
|
+
return self.status == self.CANCELLED
|
231
|
+
|
232
|
+
@property
|
233
|
+
def can_retry(self):
|
234
|
+
"""Check if this message can be retried"""
|
235
|
+
return self.is_failed and self.retry_count < self.max_retries
|
236
|
+
|
237
|
+
@property
|
238
|
+
def is_ready_to_send(self):
|
239
|
+
"""Check if this message is ready to be sent"""
|
240
|
+
if not self.is_pending:
|
241
|
+
return False
|
242
|
+
|
243
|
+
if self.scheduled_at:
|
244
|
+
from django.utils import timezone
|
245
|
+
return timezone.now() >= self.scheduled_at
|
246
|
+
|
247
|
+
return True
|
248
|
+
|
249
|
+
def mark_sending(self):
|
250
|
+
"""Mark this message as currently being sent"""
|
251
|
+
self.status = self.SENDING
|
252
|
+
self.save(update_fields=['status'])
|
253
|
+
|
254
|
+
def mark_sent(self, message_id=None):
|
255
|
+
"""Mark this message as successfully sent"""
|
256
|
+
from django.utils import timezone
|
257
|
+
self.status = self.SENT
|
258
|
+
self.sent_at = timezone.now()
|
259
|
+
if message_id:
|
260
|
+
self.message_id = message_id
|
261
|
+
self.save(update_fields=['status', 'sent_at', 'message_id'])
|
262
|
+
|
263
|
+
def mark_failed(self, error_message=None):
|
264
|
+
"""Mark this message as failed to send"""
|
265
|
+
from django.utils import timezone
|
266
|
+
self.status = self.FAILED
|
267
|
+
self.failed_at = timezone.now()
|
268
|
+
self.retry_count += 1
|
269
|
+
if error_message:
|
270
|
+
self.error_message = error_message
|
271
|
+
self.save(update_fields=['status', 'failed_at', 'retry_count', 'error_message'])
|
272
|
+
|
273
|
+
def mark_cancelled(self):
|
274
|
+
"""Mark this message as cancelled"""
|
275
|
+
self.status = self.CANCELLED
|
276
|
+
self.save(update_fields=['status'])
|
277
|
+
|
278
|
+
def reset_for_retry(self):
|
279
|
+
"""Reset message status for retry"""
|
280
|
+
if self.can_retry:
|
281
|
+
self.status = self.PENDING
|
282
|
+
self.save(update_fields=['status'])
|
283
|
+
|
284
|
+
def is_to_user(self, user):
|
285
|
+
"""Check if this message is to a specific user"""
|
286
|
+
if not user or not user.email:
|
287
|
+
return False
|
288
|
+
return self.to_address.lower() == user.email.lower()
|