python-missive 0.2.0__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 (95) hide show
  1. pymissive/__init__.py +3 -0
  2. pymissive/__main__.py +9 -0
  3. pymissive/archives/__init__.py +142 -0
  4. pymissive/archives/__main__.py +9 -0
  5. pymissive/archives/address.py +272 -0
  6. pymissive/archives/address_backends/__init__.py +29 -0
  7. pymissive/archives/address_backends/base.py +610 -0
  8. pymissive/archives/address_backends/geoapify.py +221 -0
  9. pymissive/archives/address_backends/geocode_earth.py +210 -0
  10. pymissive/archives/address_backends/google_maps.py +371 -0
  11. pymissive/archives/address_backends/here.py +348 -0
  12. pymissive/archives/address_backends/locationiq.py +271 -0
  13. pymissive/archives/address_backends/mapbox.py +314 -0
  14. pymissive/archives/address_backends/maps_co.py +257 -0
  15. pymissive/archives/address_backends/nominatim.py +348 -0
  16. pymissive/archives/address_backends/opencage.py +292 -0
  17. pymissive/archives/address_backends/pelias_mixin.py +181 -0
  18. pymissive/archives/address_backends/photon.py +322 -0
  19. pymissive/archives/cli.py +42 -0
  20. pymissive/archives/helpers.py +45 -0
  21. pymissive/archives/missive.py +64 -0
  22. pymissive/archives/providers/__init__.py +167 -0
  23. pymissive/archives/providers/apn.py +171 -0
  24. pymissive/archives/providers/ar24.py +204 -0
  25. pymissive/archives/providers/base/__init__.py +203 -0
  26. pymissive/archives/providers/base/_attachments.py +166 -0
  27. pymissive/archives/providers/base/branded.py +341 -0
  28. pymissive/archives/providers/base/common.py +781 -0
  29. pymissive/archives/providers/base/email.py +422 -0
  30. pymissive/archives/providers/base/email_message.py +85 -0
  31. pymissive/archives/providers/base/monitoring.py +150 -0
  32. pymissive/archives/providers/base/notification.py +187 -0
  33. pymissive/archives/providers/base/postal.py +742 -0
  34. pymissive/archives/providers/base/postal_defaults.py +26 -0
  35. pymissive/archives/providers/base/sms.py +213 -0
  36. pymissive/archives/providers/base/voice_call.py +82 -0
  37. pymissive/archives/providers/brevo.py +363 -0
  38. pymissive/archives/providers/certeurope.py +249 -0
  39. pymissive/archives/providers/django_email.py +182 -0
  40. pymissive/archives/providers/fcm.py +91 -0
  41. pymissive/archives/providers/laposte.py +392 -0
  42. pymissive/archives/providers/maileva.py +511 -0
  43. pymissive/archives/providers/mailgun.py +118 -0
  44. pymissive/archives/providers/messenger.py +74 -0
  45. pymissive/archives/providers/notification.py +112 -0
  46. pymissive/archives/providers/sendgrid.py +160 -0
  47. pymissive/archives/providers/ses.py +185 -0
  48. pymissive/archives/providers/signal.py +68 -0
  49. pymissive/archives/providers/slack.py +80 -0
  50. pymissive/archives/providers/smtp.py +190 -0
  51. pymissive/archives/providers/teams.py +91 -0
  52. pymissive/archives/providers/telegram.py +69 -0
  53. pymissive/archives/providers/twilio.py +310 -0
  54. pymissive/archives/providers/vonage.py +208 -0
  55. pymissive/archives/sender.py +339 -0
  56. pymissive/archives/status.py +22 -0
  57. pymissive/cli.py +42 -0
  58. pymissive/config.py +397 -0
  59. pymissive/helpers.py +0 -0
  60. pymissive/providers/apn.py +8 -0
  61. pymissive/providers/ar24.py +8 -0
  62. pymissive/providers/base/__init__.py +64 -0
  63. pymissive/providers/base/acknowledgement.py +6 -0
  64. pymissive/providers/base/attachments.py +10 -0
  65. pymissive/providers/base/branded.py +16 -0
  66. pymissive/providers/base/email.py +2 -0
  67. pymissive/providers/base/notification.py +2 -0
  68. pymissive/providers/base/postal.py +2 -0
  69. pymissive/providers/base/sms.py +2 -0
  70. pymissive/providers/base/voice_call.py +2 -0
  71. pymissive/providers/brevo.py +420 -0
  72. pymissive/providers/certeurope.py +8 -0
  73. pymissive/providers/django_email.py +8 -0
  74. pymissive/providers/fcm.py +8 -0
  75. pymissive/providers/laposte.py +8 -0
  76. pymissive/providers/maileva.py +8 -0
  77. pymissive/providers/mailgun.py +8 -0
  78. pymissive/providers/messenger.py +8 -0
  79. pymissive/providers/notification.py +8 -0
  80. pymissive/providers/partner.py +650 -0
  81. pymissive/providers/scaleway.py +498 -0
  82. pymissive/providers/sendgrid.py +8 -0
  83. pymissive/providers/ses.py +8 -0
  84. pymissive/providers/signal.py +8 -0
  85. pymissive/providers/slack.py +8 -0
  86. pymissive/providers/smtp.py +8 -0
  87. pymissive/providers/teams.py +8 -0
  88. pymissive/providers/telegram.py +8 -0
  89. pymissive/providers/twilio.py +8 -0
  90. pymissive/providers/vonage.py +8 -0
  91. python_missive-0.2.0.dist-info/METADATA +152 -0
  92. python_missive-0.2.0.dist-info/RECORD +95 -0
  93. python_missive-0.2.0.dist-info/WHEEL +5 -0
  94. python_missive-0.2.0.dist-info/entry_points.txt +2 -0
  95. python_missive-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,74 @@
1
+ """Facebook Messenger provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from ..status import MissiveStatus
8
+ from .base import BaseProvider
9
+
10
+
11
+ class MessengerProvider(BaseProvider):
12
+ """
13
+ Facebook Messenger provider.
14
+
15
+ Required configuration:
16
+ MESSENGER_PAGE_ACCESS_TOKEN: Facebook page access token
17
+ MESSENGER_APP_SECRET: Application secret
18
+
19
+ Recipient must have a PSID (Page-Scoped ID) Messenger stored in metadata.
20
+ """
21
+
22
+ name = "messenger"
23
+ display_name = "Facebook Messenger"
24
+ supported_types = ["BRANDED"]
25
+ brands = ["messenger"] # Facebook Messenger only
26
+ config_keys = ["MESSENGER_PAGE_ACCESS_TOKEN", "MESSENGER_VERIFY_TOKEN"]
27
+ required_packages = ["requests"]
28
+ site_url = "https://www.messenger.com/"
29
+ description_text = "Facebook Messenger - Consumer instant messaging (Meta)"
30
+
31
+ def validate(self) -> tuple[bool, str]:
32
+ """Validate that the recipient has a Messenger PSID"""
33
+ if not self.missive:
34
+ return False, "Missive not defined"
35
+
36
+ recipient = getattr(self.missive, "recipient", None)
37
+ if not recipient:
38
+ return False, "Recipient not defined"
39
+
40
+ metadata = getattr(recipient, "metadata", None) or {}
41
+ psid = metadata.get("messenger_psid")
42
+ if not psid:
43
+ return False, "Recipient does not have a Messenger PSID (add to metadata)"
44
+
45
+ return True, ""
46
+
47
+ def send_branded(self, brand_name: Optional[str] = None, **kwargs) -> bool:
48
+ """
49
+ Send a message via Messenger Send API.
50
+
51
+ TODO: Implement actual sending via:
52
+ POST https://graph.facebook.com/v18.0/me/messages
53
+ """
54
+ is_valid, error = self.validate()
55
+ if not is_valid:
56
+ self._update_status(MissiveStatus.FAILED, error_message=error)
57
+ return False
58
+
59
+ # TODO: Implement actual sending
60
+ external_id = f"messenger_sim_{getattr(self.missive, 'id', 'unknown')}"
61
+ self._update_status(
62
+ MissiveStatus.SENT,
63
+ external_id=external_id,
64
+ )
65
+
66
+ return True
67
+
68
+ def check_status(self, external_id: Optional[str] = None) -> Optional[str]:
69
+ """Check status via Messenger webhooks"""
70
+ # TODO: Implement webhook handlers
71
+ return None
72
+
73
+
74
+ __all__ = ["MessengerProvider"]
@@ -0,0 +1,112 @@
1
+ """In-app notification provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Dict, Optional, Tuple
7
+
8
+ from ..status import MissiveStatus
9
+ from .base import BaseProvider
10
+
11
+
12
+ class InAppNotificationProvider(BaseProvider):
13
+ """In-app notification provider."""
14
+
15
+ name = "notification"
16
+ display_name = "Notification In-App"
17
+ supported_types = ["NOTIFICATION"]
18
+ services = ["notification"]
19
+ notification_geographic_coverage = ["*"]
20
+ notification_geo = notification_geographic_coverage
21
+ required_packages = []
22
+ description_text = "In-app notifications without external dependency"
23
+
24
+ def send_notification(self, **kwargs) -> bool:
25
+ """Create an in-app notification"""
26
+ # Validation
27
+ is_valid, error = self.validate()
28
+ if not is_valid:
29
+ self._update_status(MissiveStatus.FAILED, error_message=error)
30
+ return False
31
+
32
+ if not self._get_missive_value("recipient_user"):
33
+ self._update_status(MissiveStatus.FAILED, error_message="User missing")
34
+ return False
35
+
36
+ try:
37
+ # TODO: Create the notification
38
+ # Can use:
39
+ # - Django signals
40
+ # - WebSocket (channels)
41
+ # - Firebase Cloud Messaging
42
+ # - OneSignal
43
+ #
44
+ # Example with signal:
45
+ # from django.dispatch import Signal
46
+ # notification_created = Signal()
47
+ # notification_created.send(
48
+ # sender=self.__class__,
49
+ # missive=self.missive,
50
+ # recipient=self.missive.recipient_user,
51
+ # subject=self.missive.subject,
52
+ # body=self.missive.body
53
+ # )
54
+
55
+ # Notification is instantaneous
56
+ clock = getattr(self, "_clock", None)
57
+ now = clock() if callable(clock) else datetime.now(timezone.utc)
58
+
59
+ self._update_status(
60
+ MissiveStatus.SENT,
61
+ provider=self.name,
62
+ )
63
+ # Set sent_at and delivered_at to now for instant notifications
64
+ if self.missive and hasattr(self.missive, "sent_at"):
65
+ self.missive.sent_at = now
66
+ if self.missive and hasattr(self.missive, "delivered_at"):
67
+ self.missive.delivered_at = now
68
+
69
+ self._create_event("sent", "Notification created")
70
+ self._create_event("delivered", "Notification delivered")
71
+
72
+ return True
73
+
74
+ except Exception as e:
75
+ self._update_status(MissiveStatus.FAILED, error_message=str(e))
76
+ self._create_event("failed", str(e))
77
+ return False
78
+
79
+ def validate_webhook_signature(
80
+ self,
81
+ payload: Any,
82
+ headers: Dict[str, str],
83
+ *,
84
+ missive_type: Optional[str] = None,
85
+ **kwargs: Any,
86
+ ) -> Tuple[bool, str]:
87
+ """No webhooks for in-app notifications"""
88
+ return False, "In-app notifications do not use webhooks"
89
+
90
+ def get_service_status(self) -> Dict:
91
+ """
92
+ Gets in-app notification system status.
93
+
94
+ In-app notifications are managed locally, no limits.
95
+
96
+ Returns:
97
+ Dict with status, availability, etc.
98
+ """
99
+ return self._build_generic_service_status(
100
+ status="operational",
101
+ is_available=True,
102
+ credits_type="unlimited",
103
+ rate_limits={"per_second": None},
104
+ sla={"uptime_percentage": 100.0},
105
+ details={
106
+ "provider_type": "In-App (Local)",
107
+ "note": "Notifications stockées en base de données",
108
+ },
109
+ )
110
+
111
+
112
+ __all__ = ["InAppNotificationProvider"]
@@ -0,0 +1,160 @@
1
+ """SendGrid email provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ from typing import Any, Dict, Optional, Tuple
10
+
11
+ from .base import BaseProvider
12
+
13
+
14
+ class SendGridProvider(BaseProvider):
15
+ """SendGrid email provider."""
16
+
17
+ name = "SendGrid"
18
+ display_name = "SendGrid"
19
+ supported_types = ["EMAIL", "EMAIL_MARKETING"]
20
+ # Geographic scope and pricing
21
+ email_geographic_coverage = ["*"]
22
+ email_geo = email_geographic_coverage
23
+ email_marketing_geographic_coverage = ["*"]
24
+ email_marketing_geo = email_marketing_geographic_coverage
25
+ email_price = 0.9 # Similar to Mailgun baseline
26
+ email_marketing_price = 0.15 # Marketing credits slightly higher
27
+ email_marketing_max_attachment_size_mb = 10
28
+ email_marketing_allowed_attachment_mime_types = [
29
+ "text/html",
30
+ "image/jpeg",
31
+ "image/png",
32
+ ]
33
+ config_keys = ["SENDGRID_API_KEY"]
34
+ required_packages = ["sendgrid"]
35
+ site_url = "https://sendgrid.com/"
36
+ status_url = "https://status.sendgrid.com/"
37
+ documentation_url = "https://docs.sendgrid.com/"
38
+ description_text = "Transactional and marketing email (Twilio SendGrid)"
39
+
40
+ def send_email(self, **kwargs) -> bool:
41
+ """Sends email via SendGrid API."""
42
+ return self._send_email_simulation(
43
+ prefix="sg", event_message="Email sent via SendGrid"
44
+ )
45
+
46
+ def send_email_marketing(self, **kwargs) -> bool:
47
+ """Marketing campaigns follow the transactional send path."""
48
+ return self.send_email(**kwargs)
49
+
50
+ def validate_webhook_signature(
51
+ self,
52
+ payload: Any,
53
+ headers: Dict[str, str],
54
+ *,
55
+ missive_type: Optional[str] = None,
56
+ **kwargs: Any,
57
+ ) -> Tuple[bool, str]:
58
+ """Validate SendGrid webhook signature."""
59
+ webhook_key = self._config.get("SENDGRID_WEBHOOK_KEY")
60
+ if not webhook_key:
61
+ return True, "" # No validation
62
+
63
+ sig_header = "HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_SIGNATURE"
64
+ signature = headers.get(sig_header, "")
65
+ ts_header = "HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_TIMESTAMP"
66
+ timestamp = headers.get(ts_header, "")
67
+
68
+ if not signature or not timestamp:
69
+ return False, "Signature or timestamp missing"
70
+
71
+ # Reconstruct the signature
72
+ payload_str = json.dumps(payload, separators=(",", ":"))
73
+ signed_payload = timestamp + payload_str
74
+
75
+ expected_signature = base64.b64encode(
76
+ hmac.new(
77
+ webhook_key.encode(), signed_payload.encode(), hashlib.sha256
78
+ ).digest()
79
+ ).decode()
80
+
81
+ if hmac.compare_digest(signature, expected_signature):
82
+ return True, ""
83
+ return False, "Signature does not match"
84
+
85
+ def validate_email_marketing_webhook_signature(
86
+ self,
87
+ payload: Any,
88
+ headers: Dict[str, str],
89
+ *,
90
+ missive_type: Optional[str] = None,
91
+ **kwargs: Any,
92
+ ) -> Tuple[bool, str]:
93
+ """Marketing webhooks reuse the same verification."""
94
+ return self.validate_webhook_signature(
95
+ payload, headers, missive_type="EMAIL_MARKETING", **kwargs
96
+ )
97
+
98
+ def extract_email_missive_id(self, payload: Any) -> Optional[str]:
99
+ """Extract missive ID from SendGrid webhook."""
100
+ # SendGrid sends an array of events
101
+ if isinstance(payload, list) and len(payload) > 0:
102
+ event = payload[0]
103
+ if isinstance(event, dict):
104
+ result = event.get("missive_id") or event.get("custom_args", {}).get(
105
+ "missive_id"
106
+ )
107
+ return str(result) if result else None
108
+ elif isinstance(payload, dict):
109
+ result = payload.get("missive_id") or payload.get("custom_args", {}).get(
110
+ "missive_id"
111
+ )
112
+ return str(result) if result else None
113
+ return None
114
+
115
+ def extract_email_marketing_missive_id(self, payload: Any) -> Optional[str]:
116
+ """Reuse transactional ID extraction."""
117
+ return self.extract_email_missive_id(payload)
118
+
119
+ def extract_event_type(self, payload: Any) -> str:
120
+ """Extract event type from SendGrid webhook."""
121
+ if isinstance(payload, list) and len(payload) > 0:
122
+ event = payload[0]
123
+ if isinstance(event, dict):
124
+ result = event.get("event", "unknown")
125
+ return str(result) if result else "unknown"
126
+ elif isinstance(payload, dict):
127
+ result = payload.get("event", "unknown")
128
+ return str(result) if result else "unknown"
129
+ return "unknown"
130
+
131
+ def handle_email_marketing_webhook(
132
+ self, payload: Dict[str, Any], headers: Dict[str, str]
133
+ ) -> Tuple[bool, str, Optional[Any]]:
134
+ """Delegate marketing webhook handling to transactional handler."""
135
+ return self.handle_email_webhook(payload, headers)
136
+
137
+ def get_service_status(self) -> Dict:
138
+ """
139
+ Gets SendGrid status and credits.
140
+
141
+ Returns:
142
+ Dict with status, credits, etc.
143
+ """
144
+ return self._build_generic_service_status(
145
+ credits_type="emails",
146
+ credits_currency="emails",
147
+ rate_limits={"per_second": 10, "per_minute": 600},
148
+ warnings=["SendGrid API not implemented - uncomment the code"],
149
+ details={
150
+ "status_page": "https://status.sendgrid.com/",
151
+ "api_docs": (
152
+ "https://docs.sendgrid.com/api-reference/"
153
+ "stats/retrieve-email-statistics"
154
+ ),
155
+ },
156
+ sla={"uptime_percentage": 99.99, "response_time_ms": 100},
157
+ )
158
+
159
+
160
+ __all__ = ["SendGridProvider"]
@@ -0,0 +1,185 @@
1
+ """Amazon SES email provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict
6
+
7
+ from ..status import MissiveStatus
8
+ from .base import BaseProvider
9
+
10
+
11
+ class SESProvider(BaseProvider):
12
+ """
13
+ Amazon SES (Simple Email Service) provider.
14
+
15
+ Required configuration:
16
+ AWS_ACCESS_KEY_ID: AWS access key
17
+ AWS_SECRET_ACCESS_KEY: AWS secret key
18
+ AWS_REGION: AWS region (e.g., eu-west-1, us-east-1)
19
+ SES_FROM_EMAIL: Verified sender email in SES
20
+
21
+ Supports:
22
+ - Transactional email
23
+ - Email marketing (with SES v2)
24
+ - Reputation management
25
+ """
26
+
27
+ name = "ses"
28
+ display_name = "Amazon SES"
29
+ supported_types = ["EMAIL"]
30
+ services = ["email", "email_transactional", "email_marketing"]
31
+ config_keys = [
32
+ "AWS_ACCESS_KEY_ID",
33
+ "AWS_SECRET_ACCESS_KEY",
34
+ "AWS_REGION",
35
+ "SES_FROM_EMAIL",
36
+ ]
37
+ required_packages = ["boto3"]
38
+ site_url = "https://aws.amazon.com/ses/"
39
+ status_url = "https://health.aws.amazon.com/health/status"
40
+ documentation_url = "https://docs.aws.amazon.com/ses/"
41
+ description_text = "Amazon Simple Email Service - AWS transactional email"
42
+
43
+ def send_email(self, **kwargs) -> bool:
44
+ """Send an email via Amazon SES"""
45
+ # Validation
46
+ is_valid, error = self.validate()
47
+ if not is_valid:
48
+ self._update_status(MissiveStatus.FAILED, error_message=error)
49
+ return False
50
+
51
+ if not self._get_missive_value("recipient_email"):
52
+ self._update_status(MissiveStatus.FAILED, error_message="Email missing")
53
+ return False
54
+
55
+ try:
56
+ # Configuration AWS
57
+ aws_access_key = self._config.get("AWS_ACCESS_KEY_ID")
58
+ aws_secret_key = self._config.get("AWS_SECRET_ACCESS_KEY")
59
+ self._config.get("AWS_REGION", "eu-west-1")
60
+ from_email = self._config.get("SES_FROM_EMAIL")
61
+
62
+ if not all([aws_access_key, aws_secret_key, from_email]):
63
+ self._update_status(
64
+ MissiveStatus.FAILED,
65
+ error_message="Incomplete AWS SES configuration",
66
+ )
67
+ return False
68
+
69
+ # TODO: Implement actual SES sending
70
+ # import boto3
71
+ # from botocore.exceptions import ClientError
72
+ #
73
+ # client = boto3.client(
74
+ # "ses",
75
+ # aws_access_key_id=aws_access_key,
76
+ # aws_secret_access_key=aws_secret_key,
77
+ # region_name=aws_region,
78
+ # )
79
+ #
80
+ # destination = {"ToAddresses": [self.missive.recipient_email]}
81
+ # message = {
82
+ # "Subject": {"Data": self.missive.subject, "Charset": "UTF-8"},
83
+ # "Body": {},
84
+ # }
85
+ #
86
+ # if self.missive.body_html:
87
+ # message["Body"]["Html"] = {
88
+ # "Data": self.missive.body_html,
89
+ # "Charset": "UTF-8",
90
+ # }
91
+ # if self.missive.body_text or self.missive.body:
92
+ # message["Body"]["Text"] = {
93
+ # "Data": self.missive.body_text or self.missive.body,
94
+ # "Charset": "UTF-8",
95
+ # }
96
+ #
97
+ # response = client.send_email(
98
+ # Source=from_email,
99
+ # Destination=destination,
100
+ # Message=message,
101
+ # )
102
+ #
103
+ # message_id = response.get("MessageId")
104
+
105
+ # Simulation
106
+ message_id = f"ses_{getattr(self.missive, 'id', 'unknown')}"
107
+
108
+ self._update_status(
109
+ MissiveStatus.SENT,
110
+ provider=self.name,
111
+ external_id=message_id,
112
+ )
113
+ self._create_event("sent", f"Email sent via Amazon SES (ID: {message_id})")
114
+
115
+ return True
116
+
117
+ except Exception as e:
118
+ self._update_status(MissiveStatus.FAILED, error_message=str(e))
119
+ self._create_event("failed", str(e))
120
+ return False
121
+
122
+ def get_email_service_info(self) -> Dict:
123
+ """
124
+ Gets Amazon SES service information.
125
+
126
+ Returns:
127
+ Dict with quotas, credits, reputation, etc.
128
+ """
129
+ try:
130
+ aws_access_key = self._config.get("AWS_ACCESS_KEY_ID")
131
+ aws_secret_key = self._config.get("AWS_SECRET_ACCESS_KEY")
132
+ aws_region = self._config.get("AWS_REGION", "eu-west-1")
133
+
134
+ if not all([aws_access_key, aws_secret_key]):
135
+ return {
136
+ "credits": None,
137
+ "credits_type": "quota",
138
+ "is_available": False,
139
+ "limits": {},
140
+ "warnings": ["Incomplete AWS configuration"],
141
+ "reputation": {},
142
+ "details": {},
143
+ }
144
+
145
+ # TODO: Implement actual SES API calls
146
+ # import boto3
147
+ # from botocore.exceptions import ClientError
148
+ #
149
+ # client = boto3.client(
150
+ # "ses",
151
+ # aws_access_key_id=aws_access_key,
152
+ # aws_secret_access_key=aws_secret_key,
153
+ # region_name=aws_region,
154
+ # )
155
+ #
156
+ # quota = client.get_send_quota()
157
+ # max_24h = int(quota.get("Max24HourSend", 0))
158
+ # sent_last_24h = int(quota.get("SentLast24Hours", 0))
159
+ # remaining = max_24h - sent_last_24h
160
+
161
+ return {
162
+ "credits": None,
163
+ "credits_type": "quota",
164
+ "is_available": None,
165
+ "limits": {},
166
+ "warnings": ["SES API not implemented - uncomment the code"],
167
+ "reputation": {},
168
+ "details": {
169
+ "region": aws_region,
170
+ },
171
+ }
172
+
173
+ except Exception as e:
174
+ return {
175
+ "credits": None,
176
+ "credits_type": "quota",
177
+ "is_available": False,
178
+ "limits": {},
179
+ "warnings": [f"Error: {str(e)}"],
180
+ "reputation": {},
181
+ "details": {},
182
+ }
183
+
184
+
185
+ __all__ = ["SESProvider"]
@@ -0,0 +1,68 @@
1
+ """Signal Messenger provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from ..status import MissiveStatus
8
+ from .base import BaseProvider
9
+
10
+
11
+ class SignalProvider(BaseProvider):
12
+ """
13
+ Signal Messenger provider.
14
+
15
+ Required configuration:
16
+ SIGNAL_CLI_REST_API_URL: signal-cli-rest-api URL
17
+ SIGNAL_SENDER_NUMBER: Registered sender number
18
+
19
+ Recipient must have a mobile phone number.
20
+ """
21
+
22
+ name = "signal"
23
+ display_name = "Signal"
24
+ supported_types = ["BRANDED"]
25
+ brands = ["signal"] # Signal only
26
+ config_keys = ["SIGNAL_API_KEY"]
27
+ required_packages = ["requests"]
28
+ site_url = "https://signal.org/"
29
+ description_text = "Secure end-to-end encrypted messaging"
30
+
31
+ def validate(self) -> tuple[bool, str]:
32
+ """Validate that the recipient has a mobile number"""
33
+ if not self.missive:
34
+ return False, "Missive not defined"
35
+
36
+ recipient = getattr(self.missive, "recipient", None)
37
+ if not recipient or not getattr(recipient, "mobile", None):
38
+ return False, "Recipient must have a mobile number for Signal"
39
+
40
+ return True, ""
41
+
42
+ def send_branded(self, brand_name: Optional[str] = None, **kwargs) -> bool:
43
+ """
44
+ Send a message via Signal.
45
+
46
+ TODO: Implement actual sending via signal-cli-rest-api
47
+ """
48
+ is_valid, error = self.validate()
49
+ if not is_valid:
50
+ self._update_status(MissiveStatus.FAILED, error_message=error)
51
+ return False
52
+
53
+ # TODO: Implement actual sending
54
+ external_id = f"signal_sim_{getattr(self.missive, 'id', 'unknown')}"
55
+ self._update_status(
56
+ MissiveStatus.SENT,
57
+ external_id=external_id,
58
+ )
59
+
60
+ return True
61
+
62
+ def check_status(self, external_id: Optional[str] = None) -> Optional[str]:
63
+ """Check status of a Signal message"""
64
+ # TODO: Implement if needed
65
+ return None
66
+
67
+
68
+ __all__ = ["SignalProvider"]
@@ -0,0 +1,80 @@
1
+ """Slack provider for channel and direct messaging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from ..status import MissiveStatus
8
+ from .base import BaseProvider
9
+
10
+
11
+ class SlackProvider(BaseProvider):
12
+ """Slack provider."""
13
+
14
+ name = "slack"
15
+ display_name = "Slack"
16
+ supported_types = ["BRANDED"]
17
+ services = ["slack", "messaging"]
18
+ brands = ["slack"]
19
+ config_keys = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"]
20
+ required_packages = ["slack-sdk"]
21
+ site_url = "https://slack.com/"
22
+ status_url = "https://status.slack.com/"
23
+ documentation_url = "https://api.slack.com/"
24
+ description_text = "Professional team collaboration messaging"
25
+ # Geographic scope
26
+ branded_geo = "*"
27
+
28
+ def validate(self) -> tuple[bool, str]:
29
+ """Validates recipient has Slack user_id or channel_id."""
30
+ if not self.missive:
31
+ return False, "Missive not defined"
32
+
33
+ recipient = getattr(self.missive, "recipient", None)
34
+ if not recipient:
35
+ return False, "Recipient not defined"
36
+
37
+ metadata = getattr(recipient, "metadata", None) or {}
38
+ user_id = metadata.get("slack_user_id")
39
+ channel_id = metadata.get("slack_channel_id")
40
+
41
+ if not user_id and not channel_id:
42
+ return (
43
+ False,
44
+ "Recipient must have slack_user_id or slack_channel_id in metadata",
45
+ )
46
+
47
+ return True, ""
48
+
49
+ def send_slack(self) -> bool:
50
+ """Sends Slack message via Web API."""
51
+ is_valid, error = self.validate()
52
+ if not is_valid:
53
+ self._update_status(MissiveStatus.FAILED, error_message=error)
54
+ return False
55
+
56
+ # TODO: Implement actual sending
57
+ # from slack_sdk import WebClient
58
+ #
59
+ # client = WebClient(token=self._config.get("SLACK_BOT_TOKEN"))
60
+ # response = client.chat_postMessage(
61
+ # channel=channel_id or user_id,
62
+ # text=self.missive.body_text,
63
+ # blocks=[...] # For rich formatting
64
+ # )
65
+
66
+ external_id = f"slack_sim_{getattr(self.missive, 'id', 'unknown')}"
67
+ self._update_status(
68
+ MissiveStatus.SENT,
69
+ external_id=external_id,
70
+ )
71
+
72
+ return True
73
+
74
+ def check_status(self, external_id: Optional[str] = None) -> Optional[str]:
75
+ """Checks if message was read (requires Events API)."""
76
+ # TODO: Implement via Slack Events API
77
+ return None
78
+
79
+
80
+ __all__ = ["SlackProvider"]