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.
- pymissive/__init__.py +3 -0
- pymissive/__main__.py +9 -0
- pymissive/archives/__init__.py +142 -0
- pymissive/archives/__main__.py +9 -0
- pymissive/archives/address.py +272 -0
- pymissive/archives/address_backends/__init__.py +29 -0
- pymissive/archives/address_backends/base.py +610 -0
- pymissive/archives/address_backends/geoapify.py +221 -0
- pymissive/archives/address_backends/geocode_earth.py +210 -0
- pymissive/archives/address_backends/google_maps.py +371 -0
- pymissive/archives/address_backends/here.py +348 -0
- pymissive/archives/address_backends/locationiq.py +271 -0
- pymissive/archives/address_backends/mapbox.py +314 -0
- pymissive/archives/address_backends/maps_co.py +257 -0
- pymissive/archives/address_backends/nominatim.py +348 -0
- pymissive/archives/address_backends/opencage.py +292 -0
- pymissive/archives/address_backends/pelias_mixin.py +181 -0
- pymissive/archives/address_backends/photon.py +322 -0
- pymissive/archives/cli.py +42 -0
- pymissive/archives/helpers.py +45 -0
- pymissive/archives/missive.py +64 -0
- pymissive/archives/providers/__init__.py +167 -0
- pymissive/archives/providers/apn.py +171 -0
- pymissive/archives/providers/ar24.py +204 -0
- pymissive/archives/providers/base/__init__.py +203 -0
- pymissive/archives/providers/base/_attachments.py +166 -0
- pymissive/archives/providers/base/branded.py +341 -0
- pymissive/archives/providers/base/common.py +781 -0
- pymissive/archives/providers/base/email.py +422 -0
- pymissive/archives/providers/base/email_message.py +85 -0
- pymissive/archives/providers/base/monitoring.py +150 -0
- pymissive/archives/providers/base/notification.py +187 -0
- pymissive/archives/providers/base/postal.py +742 -0
- pymissive/archives/providers/base/postal_defaults.py +26 -0
- pymissive/archives/providers/base/sms.py +213 -0
- pymissive/archives/providers/base/voice_call.py +82 -0
- pymissive/archives/providers/brevo.py +363 -0
- pymissive/archives/providers/certeurope.py +249 -0
- pymissive/archives/providers/django_email.py +182 -0
- pymissive/archives/providers/fcm.py +91 -0
- pymissive/archives/providers/laposte.py +392 -0
- pymissive/archives/providers/maileva.py +511 -0
- pymissive/archives/providers/mailgun.py +118 -0
- pymissive/archives/providers/messenger.py +74 -0
- pymissive/archives/providers/notification.py +112 -0
- pymissive/archives/providers/sendgrid.py +160 -0
- pymissive/archives/providers/ses.py +185 -0
- pymissive/archives/providers/signal.py +68 -0
- pymissive/archives/providers/slack.py +80 -0
- pymissive/archives/providers/smtp.py +190 -0
- pymissive/archives/providers/teams.py +91 -0
- pymissive/archives/providers/telegram.py +69 -0
- pymissive/archives/providers/twilio.py +310 -0
- pymissive/archives/providers/vonage.py +208 -0
- pymissive/archives/sender.py +339 -0
- pymissive/archives/status.py +22 -0
- pymissive/cli.py +42 -0
- pymissive/config.py +397 -0
- pymissive/helpers.py +0 -0
- pymissive/providers/apn.py +8 -0
- pymissive/providers/ar24.py +8 -0
- pymissive/providers/base/__init__.py +64 -0
- pymissive/providers/base/acknowledgement.py +6 -0
- pymissive/providers/base/attachments.py +10 -0
- pymissive/providers/base/branded.py +16 -0
- pymissive/providers/base/email.py +2 -0
- pymissive/providers/base/notification.py +2 -0
- pymissive/providers/base/postal.py +2 -0
- pymissive/providers/base/sms.py +2 -0
- pymissive/providers/base/voice_call.py +2 -0
- pymissive/providers/brevo.py +420 -0
- pymissive/providers/certeurope.py +8 -0
- pymissive/providers/django_email.py +8 -0
- pymissive/providers/fcm.py +8 -0
- pymissive/providers/laposte.py +8 -0
- pymissive/providers/maileva.py +8 -0
- pymissive/providers/mailgun.py +8 -0
- pymissive/providers/messenger.py +8 -0
- pymissive/providers/notification.py +8 -0
- pymissive/providers/partner.py +650 -0
- pymissive/providers/scaleway.py +498 -0
- pymissive/providers/sendgrid.py +8 -0
- pymissive/providers/ses.py +8 -0
- pymissive/providers/signal.py +8 -0
- pymissive/providers/slack.py +8 -0
- pymissive/providers/smtp.py +8 -0
- pymissive/providers/teams.py +8 -0
- pymissive/providers/telegram.py +8 -0
- pymissive/providers/twilio.py +8 -0
- pymissive/providers/vonage.py +8 -0
- python_missive-0.2.0.dist-info/METADATA +152 -0
- python_missive-0.2.0.dist-info/RECORD +95 -0
- python_missive-0.2.0.dist-info/WHEEL +5 -0
- python_missive-0.2.0.dist-info/entry_points.txt +2 -0
- python_missive-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Maileva provider for postal mail and registered mail."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple, cast
|
|
7
|
+
|
|
8
|
+
from ..status import MissiveStatus
|
|
9
|
+
from .base import BaseProvider
|
|
10
|
+
from .base.postal_defaults import (
|
|
11
|
+
POSTAL_DEFAULT_MIME_TYPES,
|
|
12
|
+
POSTAL_ENVELOPE_LIMITS,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MailevaProvider(BaseProvider):
|
|
17
|
+
"""
|
|
18
|
+
Maileva provider (Docaposte/La Poste group).
|
|
19
|
+
|
|
20
|
+
Maileva is a subsidiary of Docaposte (La Poste group) offering
|
|
21
|
+
electronic postal mail services for businesses.
|
|
22
|
+
|
|
23
|
+
Supports:
|
|
24
|
+
- Postal mail (simple, registered, signature)
|
|
25
|
+
- Qualified and standard electronic registered letters (LRE / ERE)
|
|
26
|
+
- Document archiving
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name = "Maileva"
|
|
30
|
+
display_name = "Maileva"
|
|
31
|
+
supported_types = [
|
|
32
|
+
"POSTAL",
|
|
33
|
+
"POSTAL_REGISTERED",
|
|
34
|
+
"POSTAL_SIGNATURE",
|
|
35
|
+
"LRE",
|
|
36
|
+
"LRE_QUALIFIED",
|
|
37
|
+
"ERE",
|
|
38
|
+
]
|
|
39
|
+
# Postal simple
|
|
40
|
+
postal_price = 1.435
|
|
41
|
+
postal_page_price_black_white = 0.33
|
|
42
|
+
postal_page_price_color = 0.58
|
|
43
|
+
postal_page_price_single_sided = 0.33
|
|
44
|
+
postal_page_price_duplex = 0.34
|
|
45
|
+
postal_allowed_attachment_mime_types = list(POSTAL_DEFAULT_MIME_TYPES)
|
|
46
|
+
postal_allowed_page_formats = ["A4"]
|
|
47
|
+
postal_envelope_limits = [limit.copy() for limit in POSTAL_ENVELOPE_LIMITS]
|
|
48
|
+
postal_page_limit = 45
|
|
49
|
+
postal_color_printing_available = True
|
|
50
|
+
postal_duplex_printing_available = True
|
|
51
|
+
postal_archiving_duration = 0
|
|
52
|
+
|
|
53
|
+
# Recommended postal mail
|
|
54
|
+
postal_registered_price = 5.36
|
|
55
|
+
postal_registered_page_price_black_white = 0.33
|
|
56
|
+
postal_registered_page_price_color = 0.58
|
|
57
|
+
postal_registered_page_price_single_sided = 0.33
|
|
58
|
+
postal_registered_page_price_duplex = 0.34
|
|
59
|
+
postal_registered_allowed_attachment_mime_types = list(POSTAL_DEFAULT_MIME_TYPES)
|
|
60
|
+
postal_registered_allowed_page_formats = ["A4"]
|
|
61
|
+
postal_registered_envelope_limits = [limit.copy() for limit in POSTAL_ENVELOPE_LIMITS]
|
|
62
|
+
postal_registered_page_limit = 45
|
|
63
|
+
postal_registered_color_printing_available = True
|
|
64
|
+
postal_registered_duplex_printing_available = True
|
|
65
|
+
postal_registered_archiving_duration = 0
|
|
66
|
+
|
|
67
|
+
# Postal signature
|
|
68
|
+
postal_signature_price = 6.45
|
|
69
|
+
postal_signature_page_price_black_white = 0.33
|
|
70
|
+
postal_signature_page_price_color = 0.58
|
|
71
|
+
postal_signature_page_price_single_sided = 0.33
|
|
72
|
+
postal_signature_page_price_duplex = 0.34
|
|
73
|
+
postal_signature_allowed_attachment_mime_types = list(POSTAL_DEFAULT_MIME_TYPES)
|
|
74
|
+
postal_signature_allowed_page_formats = ["A4"]
|
|
75
|
+
postal_signature_envelope_limits = [limit.copy() for limit in POSTAL_ENVELOPE_LIMITS]
|
|
76
|
+
postal_signature_page_limit = 45
|
|
77
|
+
postal_signature_color_printing_available = True
|
|
78
|
+
postal_signature_duplex_printing_available = True
|
|
79
|
+
postal_signature_archiving_duration = 0
|
|
80
|
+
|
|
81
|
+
# LRE standard
|
|
82
|
+
lre_price = 3.9
|
|
83
|
+
lre_page_price_black_white = 0.0
|
|
84
|
+
lre_page_price_color = 0.0
|
|
85
|
+
lre_page_price_single_sided = 0.0
|
|
86
|
+
lre_page_price_duplex = 0.0
|
|
87
|
+
lre_allowed_attachment_mime_types = ["application/pdf"]
|
|
88
|
+
lre_allowed_page_formats: list[str] = []
|
|
89
|
+
lre_envelope_limits: list[dict[str, Any]] = []
|
|
90
|
+
lre_page_limit = 200
|
|
91
|
+
lre_color_printing_available = False
|
|
92
|
+
lre_duplex_printing_available = False
|
|
93
|
+
lre_archiving_duration = 3650 # 10 years
|
|
94
|
+
|
|
95
|
+
# Qualified electronic registered mail
|
|
96
|
+
lre_qualified_price = 6.2
|
|
97
|
+
lre_qualified_page_price_black_white = 0.0
|
|
98
|
+
lre_qualified_page_price_color = 0.0
|
|
99
|
+
lre_qualified_page_price_single_sided = 0.0
|
|
100
|
+
lre_qualified_page_price_duplex = 0.0
|
|
101
|
+
lre_qualified_allowed_attachment_mime_types = ["application/pdf"]
|
|
102
|
+
lre_qualified_allowed_page_formats: list[str] = []
|
|
103
|
+
lre_qualified_envelope_limits: list[dict[str, Any]] = []
|
|
104
|
+
lre_qualified_page_limit = 200
|
|
105
|
+
lre_qualified_color_printing_available = False
|
|
106
|
+
lre_qualified_duplex_printing_available = False
|
|
107
|
+
lre_qualified_archiving_duration = 3650
|
|
108
|
+
|
|
109
|
+
# Electronic registered mail
|
|
110
|
+
ere_price = 2.8
|
|
111
|
+
ere_page_price_black_white = 0.0
|
|
112
|
+
ere_page_price_color = 0.0
|
|
113
|
+
ere_page_price_single_sided = 0.0
|
|
114
|
+
ere_page_price_duplex = 0.0
|
|
115
|
+
ere_allowed_attachment_mime_types = ["application/pdf", "application/xml"]
|
|
116
|
+
ere_allowed_page_formats: list[str] = []
|
|
117
|
+
ere_envelope_limits: list[dict[str, Any]] = []
|
|
118
|
+
ere_page_limit = 200
|
|
119
|
+
ere_color_printing_available = False
|
|
120
|
+
ere_duplex_printing_available = False
|
|
121
|
+
ere_archiving_duration = 1825 # 5 years
|
|
122
|
+
# Geographic scopes per service family
|
|
123
|
+
postal_geographic_coverage = ["FR"] # Postal mail limited to France
|
|
124
|
+
postal_registered_geographic_coverage = ["FR"]
|
|
125
|
+
postal_signature_geographic_coverage = ["FR"]
|
|
126
|
+
lre_geographic_coverage = ["FR"] # LRE limited to France
|
|
127
|
+
lre_qualified_geographic_coverage = ["FR"]
|
|
128
|
+
ere_geographic_coverage = ["FR"] # ERE limited to France (Docaposte trust scope)
|
|
129
|
+
email_geographic_coverage = ["FR"]
|
|
130
|
+
config_keys = [
|
|
131
|
+
"MAILEVA_CLIENTID",
|
|
132
|
+
"MAILEVA_SECRET",
|
|
133
|
+
"MAILEVA_USERNAME",
|
|
134
|
+
"MAILEVA_PASSWORD",
|
|
135
|
+
]
|
|
136
|
+
required_packages = ["requests"]
|
|
137
|
+
site_url = "https://www.maileva.com/"
|
|
138
|
+
documentation_url = "https://www.maileva.com/developpeur"
|
|
139
|
+
description_text = "Electronic postal mail and registered mail services"
|
|
140
|
+
|
|
141
|
+
# API endpoints
|
|
142
|
+
API_BASE_PRODUCTION = "https://api.maileva.com"
|
|
143
|
+
API_BASE_SANDBOX = "https://api.sandbox.maileva.net"
|
|
144
|
+
AUTH_BASE_PRODUCTION = "https://connexion.maileva.com"
|
|
145
|
+
AUTH_BASE_SANDBOX = "https://connexion.sandbox.maileva.net"
|
|
146
|
+
|
|
147
|
+
def _get_api_base(self) -> str:
|
|
148
|
+
"""Get API base URL based on sandbox mode."""
|
|
149
|
+
sandbox = self._config.get("MAILEVA_SANDBOX", False)
|
|
150
|
+
return self.API_BASE_SANDBOX if sandbox else self.API_BASE_PRODUCTION
|
|
151
|
+
|
|
152
|
+
def _get_auth_base(self) -> str:
|
|
153
|
+
"""Get authentication base URL based on sandbox mode."""
|
|
154
|
+
sandbox = self._config.get("MAILEVA_SANDBOX", False)
|
|
155
|
+
return self.AUTH_BASE_SANDBOX if sandbox else self.AUTH_BASE_PRODUCTION
|
|
156
|
+
|
|
157
|
+
def _get_access_token(self) -> Optional[str]:
|
|
158
|
+
"""
|
|
159
|
+
Get OAuth access token from Maileva.
|
|
160
|
+
|
|
161
|
+
Maileva uses OAuth 2.0 with client credentials flow.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
import requests
|
|
165
|
+
|
|
166
|
+
auth_url = f"{self._get_auth_base()}/auth/realms/services/protocol/openid-connect/token"
|
|
167
|
+
client_id = self._config.get("MAILEVA_CLIENTID")
|
|
168
|
+
client_secret = self._config.get("MAILEVA_SECRET")
|
|
169
|
+
username = self._config.get("MAILEVA_USERNAME")
|
|
170
|
+
password = self._config.get("MAILEVA_PASSWORD")
|
|
171
|
+
|
|
172
|
+
if not all([client_id, client_secret, username, password]):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# OAuth 2.0 client credentials + resource owner password credentials
|
|
176
|
+
response = requests.post(
|
|
177
|
+
auth_url,
|
|
178
|
+
data={
|
|
179
|
+
"grant_type": "password",
|
|
180
|
+
"client_id": client_id,
|
|
181
|
+
"client_secret": client_secret,
|
|
182
|
+
"username": username,
|
|
183
|
+
"password": password,
|
|
184
|
+
},
|
|
185
|
+
timeout=10,
|
|
186
|
+
)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
token_data = cast(Dict[str, Any], response.json())
|
|
189
|
+
access_token = token_data.get("access_token")
|
|
190
|
+
return str(access_token) if isinstance(access_token, str) else None
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self._create_event("error", f"Failed to get access token: {e}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _send_postal_service(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
service: str,
|
|
200
|
+
is_registered: bool = False,
|
|
201
|
+
requires_signature: bool = False,
|
|
202
|
+
**kwargs,
|
|
203
|
+
) -> bool:
|
|
204
|
+
"""Internal helper to send any postal variation."""
|
|
205
|
+
# Validation
|
|
206
|
+
is_valid, error = self.validate()
|
|
207
|
+
if not is_valid:
|
|
208
|
+
self._update_status(MissiveStatus.FAILED, error_message=error)
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
if not self._get_missive_value("recipient_address"):
|
|
212
|
+
self._update_status(MissiveStatus.FAILED, error_message="Address missing")
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
access_token = self._get_access_token()
|
|
217
|
+
if not access_token:
|
|
218
|
+
self._update_status(
|
|
219
|
+
MissiveStatus.FAILED, error_message="Failed to authenticate"
|
|
220
|
+
)
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
# TODO: Implement API call
|
|
224
|
+
# api_base = self._get_api_base()
|
|
225
|
+
# Choose API version based on service type
|
|
226
|
+
# if is_registered or requires_signature:
|
|
227
|
+
# sendings_url = f"{api_base}/registered_mail/v4/sendings"
|
|
228
|
+
# else:
|
|
229
|
+
# sendings_url = f"{api_base}/mail/v2/sendings"
|
|
230
|
+
#
|
|
231
|
+
# headers = {
|
|
232
|
+
# "Authorization": f"Bearer {access_token}",
|
|
233
|
+
# "Content-Type": "application/json",
|
|
234
|
+
# }
|
|
235
|
+
#
|
|
236
|
+
# recipient_address = self._get_missive_value("recipient_address", "")
|
|
237
|
+
# address_lines = recipient_address.split("\n") if recipient_address else []
|
|
238
|
+
# sending_data = {
|
|
239
|
+
# "sender": {...},
|
|
240
|
+
# "recipient": {...},
|
|
241
|
+
# "options": {...},
|
|
242
|
+
# }
|
|
243
|
+
# TODO: Add document upload
|
|
244
|
+
# TODO: Add recipient details
|
|
245
|
+
# TODO: Submit sending
|
|
246
|
+
|
|
247
|
+
# Simulation for now
|
|
248
|
+
external_id = f"mv_{getattr(self.missive, 'id', 'unknown')}"
|
|
249
|
+
|
|
250
|
+
letter_type = service.replace("_", " ")
|
|
251
|
+
if not letter_type:
|
|
252
|
+
letter_type = "postal"
|
|
253
|
+
|
|
254
|
+
self._update_status(
|
|
255
|
+
MissiveStatus.SENT, provider=self.name, external_id=external_id
|
|
256
|
+
)
|
|
257
|
+
self._create_event("sent", f"{letter_type} letter sent via Maileva")
|
|
258
|
+
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
self._update_status(MissiveStatus.FAILED, error_message=str(e))
|
|
263
|
+
self._create_event("failed", str(e))
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
def _send_electronic_registered(self, service: str, *, description: str) -> bool:
|
|
267
|
+
"""Simulate electronic registered (LRE/ERE) sending."""
|
|
268
|
+
is_valid, error = self.validate()
|
|
269
|
+
if not is_valid:
|
|
270
|
+
self._update_status(MissiveStatus.FAILED, error_message=error)
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
recipient_email = self._get_missive_value("recipient_email")
|
|
274
|
+
if not recipient_email:
|
|
275
|
+
self._update_status(
|
|
276
|
+
MissiveStatus.FAILED, error_message="Recipient email missing"
|
|
277
|
+
)
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
external_id = f"{service}_{getattr(self.missive, 'id', 'unknown')}"
|
|
282
|
+
self._update_status(
|
|
283
|
+
MissiveStatus.SENT, provider=self.name, external_id=external_id
|
|
284
|
+
)
|
|
285
|
+
self._create_event("sent", f"{description} sent via Maileva")
|
|
286
|
+
return True
|
|
287
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
288
|
+
self._update_status(MissiveStatus.FAILED, error_message=str(exc))
|
|
289
|
+
self._create_event("failed", str(exc))
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
def send_lre(self, **kwargs) -> bool:
|
|
293
|
+
"""Send a standard LRE via Maileva."""
|
|
294
|
+
return self._send_electronic_registered("lre", description="LRE", **kwargs)
|
|
295
|
+
|
|
296
|
+
def send_lre_qualified(self, **kwargs) -> bool:
|
|
297
|
+
"""Send a qualified LRE via Maileva."""
|
|
298
|
+
return self._send_electronic_registered(
|
|
299
|
+
"lre_qualified", description="Qualified LRE", **kwargs
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def send_ere(self, **kwargs) -> bool:
|
|
303
|
+
"""Send an electronic registered email via Maileva."""
|
|
304
|
+
return self._send_electronic_registered("ere", description="ERE", **kwargs)
|
|
305
|
+
|
|
306
|
+
def validate_webhook_signature(
|
|
307
|
+
self,
|
|
308
|
+
payload: Any,
|
|
309
|
+
headers: Dict[str, str],
|
|
310
|
+
*,
|
|
311
|
+
missive_type: Optional[str] = None,
|
|
312
|
+
**kwargs: Any,
|
|
313
|
+
) -> Tuple[bool, str]:
|
|
314
|
+
"""Validate Maileva webhook signature."""
|
|
315
|
+
# TODO: Implement according to Maileva webhook documentation
|
|
316
|
+
# Maileva webhooks may use HMAC or OAuth signature
|
|
317
|
+
return True, ""
|
|
318
|
+
|
|
319
|
+
def extract_missive_id(
|
|
320
|
+
self, payload: Any, *, missive_type: Optional[str] = None, **kwargs: Any
|
|
321
|
+
) -> Optional[str]:
|
|
322
|
+
"""Extract missive ID from Maileva webhook."""
|
|
323
|
+
if isinstance(payload, dict):
|
|
324
|
+
result = (
|
|
325
|
+
payload.get("sending_id")
|
|
326
|
+
or payload.get("reference")
|
|
327
|
+
or payload.get("id")
|
|
328
|
+
)
|
|
329
|
+
return str(result) if result else None
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
def extract_event_type(self, payload: Any) -> str:
|
|
333
|
+
"""Extract event type from Maileva webhook."""
|
|
334
|
+
if isinstance(payload, dict):
|
|
335
|
+
result = payload.get("status") or payload.get("event_type") or "unknown"
|
|
336
|
+
return str(result) if result else "unknown"
|
|
337
|
+
return "unknown"
|
|
338
|
+
|
|
339
|
+
def get_proofs_of_delivery(self, service_type: Optional[str] = None) -> list:
|
|
340
|
+
"""
|
|
341
|
+
Get all Maileva proofs.
|
|
342
|
+
|
|
343
|
+
Maileva generates:
|
|
344
|
+
- Deposit proof (global_deposit_proofs)
|
|
345
|
+
- Delivery proof (if registered)
|
|
346
|
+
- Signature proof (if signature required)
|
|
347
|
+
"""
|
|
348
|
+
if not self.missive:
|
|
349
|
+
return []
|
|
350
|
+
|
|
351
|
+
external_id = getattr(self.missive, "external_id", None)
|
|
352
|
+
if not external_id or not str(external_id).startswith("mv_"):
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
access_token = self._get_access_token()
|
|
357
|
+
if not access_token:
|
|
358
|
+
return []
|
|
359
|
+
|
|
360
|
+
api_base = self._get_api_base()
|
|
361
|
+
sending_id = str(external_id).replace("mv_", "")
|
|
362
|
+
|
|
363
|
+
# TODO: Implement real API call
|
|
364
|
+
# proofs_url = f"{api_base}/registered_mail/v4/global_deposit_proofs"
|
|
365
|
+
# headers = {"Authorization": f"Bearer {access_token}"}
|
|
366
|
+
# response = requests.get(
|
|
367
|
+
# proofs_url,
|
|
368
|
+
# params={"sending_id": sending_id},
|
|
369
|
+
# headers=headers,
|
|
370
|
+
# timeout=10,
|
|
371
|
+
# )
|
|
372
|
+
# response.raise_for_status()
|
|
373
|
+
# proofs_data = response.json()
|
|
374
|
+
|
|
375
|
+
# Simulation
|
|
376
|
+
clock = getattr(self, "_clock", None)
|
|
377
|
+
sent_at = getattr(self.missive, "sent_at", None) or (
|
|
378
|
+
clock() if callable(clock) else datetime.now(timezone.utc)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
proofs = [
|
|
382
|
+
{
|
|
383
|
+
"type": "deposit_receipt",
|
|
384
|
+
"label": "Deposit Proof",
|
|
385
|
+
"available": True,
|
|
386
|
+
"url": f"{api_base}/registered_mail/v4/global_deposit_proofs/{sending_id}",
|
|
387
|
+
"generated_at": sent_at,
|
|
388
|
+
"expires_at": None,
|
|
389
|
+
"format": "pdf",
|
|
390
|
+
"metadata": {
|
|
391
|
+
"proof_type": "deposit",
|
|
392
|
+
"provider": "maileva",
|
|
393
|
+
"sending_id": sending_id,
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
# Add delivery proof if registered
|
|
399
|
+
if getattr(self.missive, "is_registered", False):
|
|
400
|
+
delivered_at = getattr(self.missive, "delivered_at", None)
|
|
401
|
+
if delivered_at:
|
|
402
|
+
proofs.append(
|
|
403
|
+
{
|
|
404
|
+
"type": "acknowledgment_receipt",
|
|
405
|
+
"label": "Acknowledgement of Receipt",
|
|
406
|
+
"available": True,
|
|
407
|
+
"url": f"{api_base}/registered_mail/v4/sendings/{sending_id}/proofs/ar",
|
|
408
|
+
"generated_at": delivered_at,
|
|
409
|
+
"expires_at": None,
|
|
410
|
+
"format": "pdf",
|
|
411
|
+
"metadata": {
|
|
412
|
+
"proof_type": "ar",
|
|
413
|
+
"provider": "maileva",
|
|
414
|
+
"sending_id": sending_id,
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return proofs
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
self._create_event("error", f"Failed to get proofs: {e}")
|
|
423
|
+
return []
|
|
424
|
+
|
|
425
|
+
def get_service_status(self) -> Dict:
|
|
426
|
+
"""
|
|
427
|
+
Gets Maileva status and credits.
|
|
428
|
+
|
|
429
|
+
Maileva uses prepaid credits and subscription model.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Dict with status, credits, etc.
|
|
433
|
+
"""
|
|
434
|
+
return self._build_service_status_payload(
|
|
435
|
+
rate_limits={"per_second": 5, "per_minute": 300},
|
|
436
|
+
warnings=["Maileva API not fully implemented - uncomment the code"],
|
|
437
|
+
details={
|
|
438
|
+
"refill_url": "https://www.maileva.com/",
|
|
439
|
+
"api_docs": "https://www.maileva.com/developpeur",
|
|
440
|
+
"sandbox_url": "https://secure2.recette.maileva.com/",
|
|
441
|
+
},
|
|
442
|
+
sla={"uptime_percentage": 99.5},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def get_postal_service_info(self) -> Dict[str, Any]:
|
|
446
|
+
"""Get postal service information."""
|
|
447
|
+
return {
|
|
448
|
+
"provider": self.name,
|
|
449
|
+
"services": ["postal", "postal_registered", "postal_signature"],
|
|
450
|
+
"max_attachment_size_mb": 10.0,
|
|
451
|
+
"max_attachment_size_bytes": 10 * 1024 * 1024,
|
|
452
|
+
"allowed_attachment_mime_types": self.postal_allowed_attachment_mime_types,
|
|
453
|
+
"geographic_coverage": self.postal_geographic_coverage,
|
|
454
|
+
"features": [
|
|
455
|
+
"Color printing",
|
|
456
|
+
"Duplex printing",
|
|
457
|
+
"Optional address sheet",
|
|
458
|
+
"Document archiving",
|
|
459
|
+
],
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
def get_lre_service_info(self) -> Dict[str, Any]:
|
|
463
|
+
"""Describe Maileva LRE (standard) features."""
|
|
464
|
+
return {
|
|
465
|
+
"provider": self.name,
|
|
466
|
+
"services": ["lre"],
|
|
467
|
+
"geographic_coverage": self.lre_geographic_coverage,
|
|
468
|
+
"features": [
|
|
469
|
+
"Electronic registered letter creation",
|
|
470
|
+
"Delivery tracking and proofs",
|
|
471
|
+
"Integration via Maileva API catalogue",
|
|
472
|
+
],
|
|
473
|
+
"details": {
|
|
474
|
+
"catalog_url": "https://www.maileva.com/catalogue-api/envoi-et-suivi-ere-simples/",
|
|
475
|
+
},
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
def get_lre_qualified_service_info(self) -> Dict[str, Any]:
|
|
479
|
+
"""Describe qualified LRE (Docaposte trust service) capabilities."""
|
|
480
|
+
return {
|
|
481
|
+
"provider": self.name,
|
|
482
|
+
"services": ["lre_qualified"],
|
|
483
|
+
"geographic_coverage": self.lre_qualified_geographic_coverage,
|
|
484
|
+
"features": [
|
|
485
|
+
"Qualified LRE generation (eIDAS-compliant)",
|
|
486
|
+
"Qualified electronic signature + timestamp",
|
|
487
|
+
"Full traceability with legal proofs",
|
|
488
|
+
],
|
|
489
|
+
"details": {
|
|
490
|
+
"catalog_url": "https://www.maileva.com/catalogue-api/envoi-et-suivi-de-lre-qualifiees/",
|
|
491
|
+
},
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
def get_ere_service_info(self) -> Dict[str, Any]:
|
|
495
|
+
"""Describe simple electronic registered email (ERE) capabilities."""
|
|
496
|
+
return {
|
|
497
|
+
"provider": self.name,
|
|
498
|
+
"services": ["ere"],
|
|
499
|
+
"geographic_coverage": self.ere_geographic_coverage,
|
|
500
|
+
"features": [
|
|
501
|
+
"Electronic registered email dispatch",
|
|
502
|
+
"Acknowledgement of receipt via Maileva",
|
|
503
|
+
"API monitoring and follow-up",
|
|
504
|
+
],
|
|
505
|
+
"details": {
|
|
506
|
+
"catalog_url": "https://www.maileva.com/catalogue-api/envoi-et-suivi-ere-simples/",
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
__all__ = ["MailevaProvider"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Mailgun email provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
from typing import Any, Dict, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from .base import BaseProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MailgunProvider(BaseProvider):
|
|
13
|
+
"""Mailgun provider (Email only)."""
|
|
14
|
+
|
|
15
|
+
name = "Mailgun"
|
|
16
|
+
display_name = "Mailgun"
|
|
17
|
+
supported_types = ["EMAIL", "EMAIL_MARKETING"]
|
|
18
|
+
# Geographic scope and pricing
|
|
19
|
+
email_geographic_coverage = ["*"]
|
|
20
|
+
email_geo = email_geographic_coverage
|
|
21
|
+
email_marketing_geographic_coverage = ["*"]
|
|
22
|
+
email_marketing_geo = email_marketing_geographic_coverage
|
|
23
|
+
email_price = 0.90 # $0.80/100 emails ~ €0.009 -> scaled to €0.009, rounding
|
|
24
|
+
email_marketing_price = 0.12 # Marketing campaigns at slightly higher cost
|
|
25
|
+
email_marketing_max_attachment_size_mb = 10
|
|
26
|
+
email_marketing_allowed_attachment_mime_types = [
|
|
27
|
+
"text/html",
|
|
28
|
+
"image/jpeg",
|
|
29
|
+
"image/png",
|
|
30
|
+
]
|
|
31
|
+
config_keys = ["MAILGUN_API_KEY", "MAILGUN_DOMAIN"]
|
|
32
|
+
required_packages = ["mailgun"]
|
|
33
|
+
site_url = "https://www.mailgun.com/"
|
|
34
|
+
status_url = "https://status.mailgun.com/"
|
|
35
|
+
documentation_url = "https://documentation.mailgun.com/"
|
|
36
|
+
description_text = (
|
|
37
|
+
"Transactional email service with advanced validation and routing"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def send_email(self, **kwargs) -> bool:
|
|
41
|
+
"""Send via Mailgun API"""
|
|
42
|
+
return self._send_email_simulation(
|
|
43
|
+
prefix="mg", event_message="Email sent via Mailgun"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def send_email_marketing(self, **kwargs) -> bool:
|
|
47
|
+
"""Reuse transactional pipeline for marketing blasts."""
|
|
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 Mailgun webhook signature."""
|
|
59
|
+
api_key = self._config.get("MAILGUN_API_KEY")
|
|
60
|
+
if not api_key:
|
|
61
|
+
return True, ""
|
|
62
|
+
|
|
63
|
+
signature_data = payload.get("signature", {})
|
|
64
|
+
timestamp = signature_data.get("timestamp", "")
|
|
65
|
+
token = signature_data.get("token", "")
|
|
66
|
+
signature = signature_data.get("signature", "")
|
|
67
|
+
|
|
68
|
+
expected_signature = hmac.new(
|
|
69
|
+
api_key.encode(), f"{timestamp}{token}".encode(), hashlib.sha256
|
|
70
|
+
).hexdigest()
|
|
71
|
+
|
|
72
|
+
if hmac.compare_digest(signature, expected_signature):
|
|
73
|
+
return True, ""
|
|
74
|
+
return False, "Signature does not match"
|
|
75
|
+
|
|
76
|
+
def extract_email_missive_id(self, payload: Any) -> Optional[str]:
|
|
77
|
+
"""Extract missive ID from Mailgun webhook."""
|
|
78
|
+
if isinstance(payload, dict):
|
|
79
|
+
event_data = payload.get("event-data", {})
|
|
80
|
+
if isinstance(event_data, dict):
|
|
81
|
+
user_variables = event_data.get("user-variables", {})
|
|
82
|
+
if isinstance(user_variables, dict):
|
|
83
|
+
result = user_variables.get("missive_id")
|
|
84
|
+
return str(result) if result else None
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def extract_event_type(self, payload: Any) -> str:
|
|
88
|
+
"""Extract event type from Mailgun webhook."""
|
|
89
|
+
if isinstance(payload, dict):
|
|
90
|
+
event_data = payload.get("event-data", {})
|
|
91
|
+
if isinstance(event_data, dict):
|
|
92
|
+
result = event_data.get("event", "unknown")
|
|
93
|
+
return str(result) if result else "unknown"
|
|
94
|
+
return "unknown"
|
|
95
|
+
|
|
96
|
+
def get_service_status(self) -> Dict:
|
|
97
|
+
"""
|
|
98
|
+
Gets Mailgun status and credits.
|
|
99
|
+
|
|
100
|
+
Mailgun charges per email sent.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict with status, credits, etc.
|
|
104
|
+
"""
|
|
105
|
+
return self._build_generic_service_status(
|
|
106
|
+
credits_type="emails",
|
|
107
|
+
credits_currency="emails",
|
|
108
|
+
rate_limits={"per_second": 100, "per_minute": 6000},
|
|
109
|
+
warnings=["Mailgun API not implemented - uncomment the code"],
|
|
110
|
+
details={
|
|
111
|
+
"status_page": "https://status.mailgun.com/",
|
|
112
|
+
"api_docs": "https://documentation.mailgun.com/en/latest/api-stats.html",
|
|
113
|
+
},
|
|
114
|
+
sla={"uptime_percentage": 99.99},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
__all__ = ["MailgunProvider"]
|