mailifica 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mailifica
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: mailifica
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for Mailifica Email Infrastructure
5
+ Home-page: https://github.com/mailifica/mailifica-python
6
+ Author: Mailifica
7
+ Author-email: Mailifica <team@mailifica.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://mailifica.com
10
+ Project-URL: Documentation, https://mailifica.com/docs
11
+ Project-URL: Repository, https://github.com/mailifica/mailifica-python
12
+ Project-URL: Bug Tracker, https://github.com/mailifica/mailifica-python/issues
13
+ Keywords: email,transactional-email,mailifica,sdk,api
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: typing-extensions>=4.0.0; python_version < "3.10"
28
+ Dynamic: author
29
+ Dynamic: home-page
30
+ Dynamic: license-file
31
+ Dynamic: requires-python
32
+
33
+ # Mailifica Python SDK (`mailifica`)
34
+
35
+ > SDK oficial em Python para a infraestrutura de e-mails transacionais **Mailifica**, com interface drop-in replacement compatível com o Resend.
36
+
37
+ ---
38
+
39
+ ## 📦 Instalação
40
+
41
+ ```bash
42
+ pip install mailifica
43
+ # ou com Poetry
44
+ poetry add mailifica
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 🚀 Como Usar
50
+
51
+ ### 1. Envio Estático (Estilo Resend)
52
+
53
+ ```python
54
+ import mailifica
55
+
56
+ # Configurar API Key
57
+ mailifica.api_key = "ma_live_123456789"
58
+
59
+ params: mailifica.Emails.SendParams = {
60
+ "from": "onboarding@suaempresa.ao",
61
+ "to": ["cliente@gmail.com"],
62
+ "subject": "Boas-vindas!",
63
+ "html": "<h1>Olá!</h1><p>Seu e-mail transacional foi entregue.</p>",
64
+ }
65
+
66
+ email = mailifica.Emails.send(params)
67
+ print("E-mail ID:", email["id"])
68
+ ```
69
+
70
+ ### 2. Envio Orientado a Objetos / Instância
71
+
72
+ ```python
73
+ from mailifica import Mailifica
74
+
75
+ client = Mailifica("ma_live_123456789")
76
+
77
+ response = client.emails.send({
78
+ "from": "suporte@suaempresa.ao",
79
+ "to": "usuario@empresa.com",
80
+ "subject": "Notificação",
81
+ "text": "Seu chamado foi atualizado.",
82
+ })
83
+ ```
84
+
85
+ ### 3. Envio em Lote (Batch)
86
+
87
+ ```python
88
+ import mailifica
89
+
90
+ mailifica.api_key = "ma_live_123456789"
91
+
92
+ batch_response = mailifica.Batch.send([
93
+ {
94
+ "from": "novidades@suaempresa.ao",
95
+ "to": "cliente1@gmail.com",
96
+ "subject": "Atualização Mensal",
97
+ "html": "<p>Novidades do mês</p>",
98
+ },
99
+ {
100
+ "from": "novidades@suaempresa.ao",
101
+ "to": "cliente2@gmail.com",
102
+ "subject": "Atualização Mensal",
103
+ "html": "<p>Novidades do mês</p>",
104
+ }
105
+ ])
106
+ ```
107
+
108
+ ### 4. Gestão de Domínios & API Keys
109
+
110
+ ```python
111
+ import mailifica
112
+
113
+ # Criar domínio
114
+ domain = mailifica.Domains.create({"name": "meudominio.ao"})
115
+
116
+ # Verificar registros DNS
117
+ verified = mailifica.Domains.verify(domain["id"])
118
+
119
+ # Listar API Keys
120
+ keys = mailifica.ApiKeys.list()
121
+ ```
122
+
123
+ ### 5. Validação de Webhooks HMAC
124
+
125
+ ```python
126
+ from mailifica import Webhooks
127
+
128
+ is_valid = Webhooks.verify_signature(
129
+ payload=raw_body,
130
+ signature=headers.get("mailifica-signature"),
131
+ secret="seu_webhook_secret"
132
+ )
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 📄 Licença
138
+
139
+ MIT © [Mailifica](https://mailifica.com)
@@ -0,0 +1,107 @@
1
+ # Mailifica Python SDK (`mailifica`)
2
+
3
+ > SDK oficial em Python para a infraestrutura de e-mails transacionais **Mailifica**, com interface drop-in replacement compatível com o Resend.
4
+
5
+ ---
6
+
7
+ ## 📦 Instalação
8
+
9
+ ```bash
10
+ pip install mailifica
11
+ # ou com Poetry
12
+ poetry add mailifica
13
+ ```
14
+
15
+ ---
16
+
17
+ ## 🚀 Como Usar
18
+
19
+ ### 1. Envio Estático (Estilo Resend)
20
+
21
+ ```python
22
+ import mailifica
23
+
24
+ # Configurar API Key
25
+ mailifica.api_key = "ma_live_123456789"
26
+
27
+ params: mailifica.Emails.SendParams = {
28
+ "from": "onboarding@suaempresa.ao",
29
+ "to": ["cliente@gmail.com"],
30
+ "subject": "Boas-vindas!",
31
+ "html": "<h1>Olá!</h1><p>Seu e-mail transacional foi entregue.</p>",
32
+ }
33
+
34
+ email = mailifica.Emails.send(params)
35
+ print("E-mail ID:", email["id"])
36
+ ```
37
+
38
+ ### 2. Envio Orientado a Objetos / Instância
39
+
40
+ ```python
41
+ from mailifica import Mailifica
42
+
43
+ client = Mailifica("ma_live_123456789")
44
+
45
+ response = client.emails.send({
46
+ "from": "suporte@suaempresa.ao",
47
+ "to": "usuario@empresa.com",
48
+ "subject": "Notificação",
49
+ "text": "Seu chamado foi atualizado.",
50
+ })
51
+ ```
52
+
53
+ ### 3. Envio em Lote (Batch)
54
+
55
+ ```python
56
+ import mailifica
57
+
58
+ mailifica.api_key = "ma_live_123456789"
59
+
60
+ batch_response = mailifica.Batch.send([
61
+ {
62
+ "from": "novidades@suaempresa.ao",
63
+ "to": "cliente1@gmail.com",
64
+ "subject": "Atualização Mensal",
65
+ "html": "<p>Novidades do mês</p>",
66
+ },
67
+ {
68
+ "from": "novidades@suaempresa.ao",
69
+ "to": "cliente2@gmail.com",
70
+ "subject": "Atualização Mensal",
71
+ "html": "<p>Novidades do mês</p>",
72
+ }
73
+ ])
74
+ ```
75
+
76
+ ### 4. Gestão de Domínios & API Keys
77
+
78
+ ```python
79
+ import mailifica
80
+
81
+ # Criar domínio
82
+ domain = mailifica.Domains.create({"name": "meudominio.ao"})
83
+
84
+ # Verificar registros DNS
85
+ verified = mailifica.Domains.verify(domain["id"])
86
+
87
+ # Listar API Keys
88
+ keys = mailifica.ApiKeys.list()
89
+ ```
90
+
91
+ ### 5. Validação de Webhooks HMAC
92
+
93
+ ```python
94
+ from mailifica import Webhooks
95
+
96
+ is_valid = Webhooks.verify_signature(
97
+ payload=raw_body,
98
+ signature=headers.get("mailifica-signature"),
99
+ secret="seu_webhook_secret"
100
+ )
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 📄 Licença
106
+
107
+ MIT © [Mailifica](https://mailifica.com)
@@ -0,0 +1,44 @@
1
+ import os
2
+ from typing import Optional
3
+ from mailifica.emails import Emails
4
+ from mailifica.batch import Batch
5
+ from mailifica.domains import Domains
6
+ from mailifica.api_keys import ApiKeys
7
+ from mailifica.webhooks import Webhooks
8
+ from mailifica.errors import (
9
+ MailificaError,
10
+ AuthenticationError,
11
+ InvalidRequestError,
12
+ RateLimitError,
13
+ InternalServerError,
14
+ )
15
+ from mailifica._version import __version__
16
+
17
+ api_key: Optional[str] = os.environ.get("MAILIFICA_API_KEY")
18
+ api_url: Optional[str] = os.environ.get("MAILIFICA_BASE_URL")
19
+
20
+ class Mailifica:
21
+ def __init__(self, api_key: Optional[str] = None):
22
+ self.api_key = api_key or os.environ.get("MAILIFICA_API_KEY")
23
+ self.emails = Emails(api_key=self.api_key)
24
+ self.batch = Batch(api_key=self.api_key)
25
+ self.domains = Domains(api_key=self.api_key)
26
+ self.api_keys = ApiKeys(api_key=self.api_key)
27
+ self.webhooks = Webhooks()
28
+
29
+ __all__ = [
30
+ "Mailifica",
31
+ "Emails",
32
+ "Batch",
33
+ "Domains",
34
+ "ApiKeys",
35
+ "Webhooks",
36
+ "MailificaError",
37
+ "AuthenticationError",
38
+ "InvalidRequestError",
39
+ "RateLimitError",
40
+ "InternalServerError",
41
+ "api_key",
42
+ "api_url",
43
+ "__version__",
44
+ ]
@@ -0,0 +1,89 @@
1
+ import os
2
+ import requests
3
+ from typing import Any, Dict, Optional
4
+ from mailifica.errors import (
5
+ MailificaError,
6
+ AuthenticationError,
7
+ InvalidRequestError,
8
+ RateLimitError,
9
+ InternalServerError,
10
+ )
11
+
12
+ DEFAULT_BASE_URL = "https://api.mailifica.com/v1"
13
+
14
+ class HttpClient:
15
+ @staticmethod
16
+ def get_api_key(api_key: Optional[str] = None) -> str:
17
+ from mailifica import api_key as global_key
18
+ key = api_key or global_key or os.environ.get("MAILIFICA_API_KEY")
19
+ if not key:
20
+ raise AuthenticationError("No API key provided. Set mailifica.api_key or pass api_key parameter.")
21
+ return key
22
+
23
+ @staticmethod
24
+ def get_base_url(base_url: Optional[str] = None) -> str:
25
+ from mailifica import api_url as global_url
26
+ return (base_url or global_url or os.environ.get("MAILIFICA_BASE_URL", DEFAULT_BASE_URL)).rstrip("/")
27
+
28
+ @classmethod
29
+ def request(
30
+ cls,
31
+ method: str,
32
+ path: str,
33
+ params: Optional[Any] = None,
34
+ api_key: Optional[str] = None,
35
+ base_url: Optional[str] = None,
36
+ ) -> Dict[str, Any]:
37
+ key = cls.get_api_key(api_key)
38
+ url = f"{cls.get_base_url(base_url)}/{path.lstrip('/')}"
39
+
40
+ headers = {
41
+ "Authorization": f"Bearer {key}",
42
+ "Content-Type": "application/json",
43
+ "User-Agent": "mailifica-python/1.0.0",
44
+ }
45
+
46
+ try:
47
+ response = requests.request(
48
+ method=method,
49
+ url=url,
50
+ json=params if method in ["POST", "PUT", "PATCH", "DELETE"] and params is not None else None,
51
+ params=params if method == "GET" and isinstance(params, dict) else None,
52
+ headers=headers,
53
+ timeout=30,
54
+ )
55
+
56
+ if not response.ok:
57
+ try:
58
+ err_json = response.json()
59
+ message = (
60
+ err_json.get("error", {}).get("message")
61
+ if isinstance(err_json.get("error"), dict)
62
+ else err_json.get("message") or response.text
63
+ )
64
+ err_type = (
65
+ err_json.get("error", {}).get("code")
66
+ if isinstance(err_json.get("error"), dict)
67
+ else err_json.get("error")
68
+ )
69
+ except Exception:
70
+ message = response.text
71
+ err_type = "unknown_error"
72
+
73
+ if response.status_code == 401 or response.status_code == 403:
74
+ raise AuthenticationError(message, status_code=response.status_code, error_type=err_type)
75
+ elif response.status_code == 400 or response.status_code == 422:
76
+ raise InvalidRequestError(message, status_code=response.status_code, error_type=err_type)
77
+ elif response.status_code == 429:
78
+ raise RateLimitError(message, status_code=429, error_type=err_type)
79
+ elif response.status_code >= 500:
80
+ raise InternalServerError(message, status_code=response.status_code, error_type=err_type)
81
+ else:
82
+ raise MailificaError(message, status_code=response.status_code, error_type=err_type)
83
+
84
+ if response.status_code == 204 or not response.content:
85
+ return {"success": True}
86
+
87
+ return response.json()
88
+ except requests.RequestException as e:
89
+ raise MailificaError(f"Network error: {str(e)}")
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,40 @@
1
+ from typing import Any, Dict, Optional
2
+ from mailifica._client import HttpClient
3
+
4
+ class ApiKeys:
5
+ def __init__(self, api_key: Optional[str] = None):
6
+ self.api_key = api_key
7
+
8
+ def create(self, params: Dict[str, Any]) -> Dict[str, Any]:
9
+ return ApiKeys.create_api_key(params, api_key=self.api_key)
10
+
11
+ def list(self) -> Dict[str, Any]:
12
+ return ApiKeys.list_api_keys(api_key=self.api_key)
13
+
14
+ def remove(self, api_key_id: str) -> Dict[str, Any]:
15
+ return ApiKeys.remove_api_key(api_key_id, api_key=self.api_key)
16
+
17
+ @classmethod
18
+ def create_api_key(cls, params: Dict[str, Any], api_key: Optional[str] = None) -> Dict[str, Any]:
19
+ return HttpClient.request("POST", "/api-keys", params=params, api_key=api_key)
20
+
21
+ @classmethod
22
+ def list_api_keys(cls, api_key: Optional[str] = None) -> Dict[str, Any]:
23
+ return HttpClient.request("GET", "/api-keys", api_key=api_key)
24
+
25
+ @classmethod
26
+ def remove_api_key(cls, api_key_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
27
+ return HttpClient.request("DELETE", f"/api-keys/{api_key_id}", api_key=api_key)
28
+
29
+ # Static aliases
30
+ @classmethod
31
+ def create(cls, params: Dict[str, Any], api_key: Optional[str] = None) -> Dict[str, Any]:
32
+ return cls.create_api_key(params, api_key=api_key)
33
+
34
+ @classmethod
35
+ def list(cls, api_key: Optional[str] = None) -> Dict[str, Any]:
36
+ return cls.list_api_keys(api_key=api_key)
37
+
38
+ @classmethod
39
+ def remove(cls, api_key_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
40
+ return cls.remove_api_key(api_key_id, api_key=api_key)
@@ -0,0 +1,32 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from mailifica._client import HttpClient
3
+
4
+ class Batch:
5
+ def __init__(self, api_key: Optional[str] = None):
6
+ self.api_key = api_key
7
+
8
+ def send(self, params: List[Dict[str, Any]]) -> Dict[str, Any]:
9
+ return Batch.send_batch(params, api_key=self.api_key)
10
+
11
+ @classmethod
12
+ def send_batch(cls, params: List[Dict[str, Any]], api_key: Optional[str] = None) -> Dict[str, Any]:
13
+ normalized = []
14
+ for item in params:
15
+ d = dict(item)
16
+ if "from_email" in d:
17
+ d["from"] = d.pop("from_email")
18
+ if "from_" in d:
19
+ d["from"] = d.pop("from_")
20
+ if isinstance(d.get("to"), str):
21
+ d["to"] = [d["to"]]
22
+ if isinstance(d.get("cc"), str):
23
+ d["cc"] = [d["cc"]]
24
+ if isinstance(d.get("bcc"), str):
25
+ d["bcc"] = [d["bcc"]]
26
+ normalized.append(d)
27
+ return HttpClient.request("POST", "/emails/batch", params=normalized, api_key=api_key)
28
+
29
+ # Class-level static alias
30
+ @classmethod
31
+ def send(cls, params: List[Dict[str, Any]], api_key: Optional[str] = None) -> Dict[str, Any]:
32
+ return cls.send_batch(params, api_key=api_key)
@@ -0,0 +1,62 @@
1
+ from typing import Any, Dict, Optional
2
+ from mailifica._client import HttpClient
3
+
4
+ class Domains:
5
+ def __init__(self, api_key: Optional[str] = None):
6
+ self.api_key = api_key
7
+
8
+ def create(self, params: Dict[str, Any]) -> Dict[str, Any]:
9
+ return Domains.create_domain(params, api_key=self.api_key)
10
+
11
+ def list(self) -> Dict[str, Any]:
12
+ return Domains.list_domains(api_key=self.api_key)
13
+
14
+ def get(self, domain_id: str) -> Dict[str, Any]:
15
+ return Domains.get_domain(domain_id, api_key=self.api_key)
16
+
17
+ def verify(self, domain_id: str) -> Dict[str, Any]:
18
+ return Domains.verify_domain(domain_id, api_key=self.api_key)
19
+
20
+ def remove(self, domain_id: str) -> Dict[str, Any]:
21
+ return Domains.remove_domain(domain_id, api_key=self.api_key)
22
+
23
+ @classmethod
24
+ def create_domain(cls, params: Dict[str, Any], api_key: Optional[str] = None) -> Dict[str, Any]:
25
+ return HttpClient.request("POST", "/domains", params=params, api_key=api_key)
26
+
27
+ @classmethod
28
+ def list_domains(cls, api_key: Optional[str] = None) -> Dict[str, Any]:
29
+ return HttpClient.request("GET", "/domains", api_key=api_key)
30
+
31
+ @classmethod
32
+ def get_domain(cls, domain_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
33
+ return HttpClient.request("GET", f"/domains/{domain_id}", api_key=api_key)
34
+
35
+ @classmethod
36
+ def verify_domain(cls, domain_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
37
+ return HttpClient.request("POST", f"/domains/{domain_id}/verify", api_key=api_key)
38
+
39
+ @classmethod
40
+ def remove_domain(cls, domain_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
41
+ return HttpClient.request("DELETE", f"/domains/{domain_id}", api_key=api_key)
42
+
43
+ # Static aliases
44
+ @classmethod
45
+ def create(cls, params: Dict[str, Any], api_key: Optional[str] = None) -> Dict[str, Any]:
46
+ return cls.create_domain(params, api_key=api_key)
47
+
48
+ @classmethod
49
+ def list(cls, api_key: Optional[str] = None) -> Dict[str, Any]:
50
+ return cls.list_domains(api_key=api_key)
51
+
52
+ @classmethod
53
+ def get(cls, domain_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
54
+ return cls.get_domain(domain_id, api_key=api_key)
55
+
56
+ @classmethod
57
+ def verify(cls, domain_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
58
+ return cls.verify_domain(domain_id, api_key=api_key)
59
+
60
+ @classmethod
61
+ def remove(cls, domain_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
62
+ return cls.remove_domain(domain_id, api_key=api_key)
@@ -0,0 +1,68 @@
1
+ from typing import Any, Dict, List, Optional, Union
2
+ from typing_extensions import TypedDict
3
+ from mailifica._client import HttpClient
4
+
5
+ class Tag(TypedDict, total=False):
6
+ name: str
7
+ value: str
8
+
9
+ class Attachment(TypedDict, total=False):
10
+ content: Union[str, bytes]
11
+ filename: str
12
+ content_type: str
13
+ path: str
14
+
15
+ class SendParams(TypedDict, total=False):
16
+ from_email: str
17
+ from_: str
18
+ to: Union[str, List[str]]
19
+ subject: str
20
+ html: Optional[str]
21
+ text: Optional[str]
22
+ cc: Optional[Union[str, List[str]]]
23
+ bcc: Optional[Union[str, List[str]]]
24
+ reply_to: Optional[Union[str, List[str]]]
25
+ headers: Optional[Dict[str, str]]
26
+ attachments: Optional[List[Attachment]]
27
+ tags: Optional[List[Tag]]
28
+ scheduled_at: Optional[str]
29
+
30
+ class Emails:
31
+ SendParams = SendParams
32
+
33
+ def __init__(self, api_key: Optional[str] = None):
34
+ self.api_key = api_key
35
+
36
+ def send(self, params: Union[SendParams, Dict[str, Any]]) -> Dict[str, Any]:
37
+ return Emails.send_email(params, api_key=self.api_key)
38
+
39
+ def get(self, email_id: str) -> Dict[str, Any]:
40
+ return Emails.get_email(email_id, api_key=self.api_key)
41
+
42
+ @classmethod
43
+ def send_email(cls, params: Union[SendParams, Dict[str, Any]], api_key: Optional[str] = None) -> Dict[str, Any]:
44
+ payload = dict(params)
45
+ if "from" in payload:
46
+ payload["from"] = payload.pop("from")
47
+ elif "from_email" in payload:
48
+ payload["from"] = payload.pop("from_email")
49
+ elif "from_" in payload:
50
+ payload["from"] = payload.pop("from_")
51
+
52
+ if isinstance(payload.get("to"), str):
53
+ payload["to"] = [payload["to"]]
54
+ if isinstance(payload.get("cc"), str):
55
+ payload["cc"] = [payload["cc"]]
56
+ if isinstance(payload.get("bcc"), str):
57
+ payload["bcc"] = [payload["bcc"]]
58
+
59
+ return HttpClient.request("POST", "/emails", params=payload, api_key=api_key)
60
+
61
+ # Class-level static aliases (Resend syntax: resend.Emails.send(...))
62
+ @classmethod
63
+ def send(cls, params: Union[SendParams, Dict[str, Any]], api_key: Optional[str] = None) -> Dict[str, Any]:
64
+ return cls.send_email(params, api_key=api_key)
65
+
66
+ @classmethod
67
+ def get(cls, email_id: str, api_key: Optional[str] = None) -> Dict[str, Any]:
68
+ return HttpClient.request("GET", f"/emails/{email_id}", api_key=api_key)
@@ -0,0 +1,20 @@
1
+ from typing import Optional
2
+
3
+ class MailificaError(Exception):
4
+ def __init__(self, message: str, status_code: Optional[int] = None, error_type: Optional[str] = None):
5
+ super().__init__(message)
6
+ self.message = message
7
+ self.status_code = status_code
8
+ self.error_type = error_type
9
+
10
+ class AuthenticationError(MailificaError):
11
+ pass
12
+
13
+ class InvalidRequestError(MailificaError):
14
+ pass
15
+
16
+ class RateLimitError(MailificaError):
17
+ pass
18
+
19
+ class InternalServerError(MailificaError):
20
+ pass
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561
@@ -0,0 +1,34 @@
1
+ import hmac
2
+ import hashlib
3
+ import json
4
+ from typing import Any, Dict, Union
5
+ from mailifica.errors import InvalidRequestError
6
+
7
+ class Webhooks:
8
+ @staticmethod
9
+ def verify_signature(payload: Union[str, bytes], signature: str, secret: str) -> bool:
10
+ if not signature or not secret:
11
+ return False
12
+
13
+ if isinstance(payload, str):
14
+ body_bytes = payload.encode("utf-8")
15
+ else:
16
+ body_bytes = payload
17
+
18
+ expected_sig = hmac.new(
19
+ secret.encode("utf-8"),
20
+ body_bytes,
21
+ hashlib.sha256
22
+ ).hexdigest()
23
+
24
+ return hmac.compare_digest(signature, expected_sig)
25
+
26
+ @classmethod
27
+ def construct_event(cls, payload: Union[str, bytes], signature: str, secret: str) -> Dict[str, Any]:
28
+ if not cls.verify_signature(payload, signature, secret):
29
+ raise InvalidRequestError("Invalid webhook HMAC signature.")
30
+
31
+ if isinstance(payload, bytes):
32
+ payload = payload.decode("utf-8")
33
+
34
+ return json.loads(payload)
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: mailifica
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for Mailifica Email Infrastructure
5
+ Home-page: https://github.com/mailifica/mailifica-python
6
+ Author: Mailifica
7
+ Author-email: Mailifica <team@mailifica.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://mailifica.com
10
+ Project-URL: Documentation, https://mailifica.com/docs
11
+ Project-URL: Repository, https://github.com/mailifica/mailifica-python
12
+ Project-URL: Bug Tracker, https://github.com/mailifica/mailifica-python/issues
13
+ Keywords: email,transactional-email,mailifica,sdk,api
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: typing-extensions>=4.0.0; python_version < "3.10"
28
+ Dynamic: author
29
+ Dynamic: home-page
30
+ Dynamic: license-file
31
+ Dynamic: requires-python
32
+
33
+ # Mailifica Python SDK (`mailifica`)
34
+
35
+ > SDK oficial em Python para a infraestrutura de e-mails transacionais **Mailifica**, com interface drop-in replacement compatível com o Resend.
36
+
37
+ ---
38
+
39
+ ## 📦 Instalação
40
+
41
+ ```bash
42
+ pip install mailifica
43
+ # ou com Poetry
44
+ poetry add mailifica
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 🚀 Como Usar
50
+
51
+ ### 1. Envio Estático (Estilo Resend)
52
+
53
+ ```python
54
+ import mailifica
55
+
56
+ # Configurar API Key
57
+ mailifica.api_key = "ma_live_123456789"
58
+
59
+ params: mailifica.Emails.SendParams = {
60
+ "from": "onboarding@suaempresa.ao",
61
+ "to": ["cliente@gmail.com"],
62
+ "subject": "Boas-vindas!",
63
+ "html": "<h1>Olá!</h1><p>Seu e-mail transacional foi entregue.</p>",
64
+ }
65
+
66
+ email = mailifica.Emails.send(params)
67
+ print("E-mail ID:", email["id"])
68
+ ```
69
+
70
+ ### 2. Envio Orientado a Objetos / Instância
71
+
72
+ ```python
73
+ from mailifica import Mailifica
74
+
75
+ client = Mailifica("ma_live_123456789")
76
+
77
+ response = client.emails.send({
78
+ "from": "suporte@suaempresa.ao",
79
+ "to": "usuario@empresa.com",
80
+ "subject": "Notificação",
81
+ "text": "Seu chamado foi atualizado.",
82
+ })
83
+ ```
84
+
85
+ ### 3. Envio em Lote (Batch)
86
+
87
+ ```python
88
+ import mailifica
89
+
90
+ mailifica.api_key = "ma_live_123456789"
91
+
92
+ batch_response = mailifica.Batch.send([
93
+ {
94
+ "from": "novidades@suaempresa.ao",
95
+ "to": "cliente1@gmail.com",
96
+ "subject": "Atualização Mensal",
97
+ "html": "<p>Novidades do mês</p>",
98
+ },
99
+ {
100
+ "from": "novidades@suaempresa.ao",
101
+ "to": "cliente2@gmail.com",
102
+ "subject": "Atualização Mensal",
103
+ "html": "<p>Novidades do mês</p>",
104
+ }
105
+ ])
106
+ ```
107
+
108
+ ### 4. Gestão de Domínios & API Keys
109
+
110
+ ```python
111
+ import mailifica
112
+
113
+ # Criar domínio
114
+ domain = mailifica.Domains.create({"name": "meudominio.ao"})
115
+
116
+ # Verificar registros DNS
117
+ verified = mailifica.Domains.verify(domain["id"])
118
+
119
+ # Listar API Keys
120
+ keys = mailifica.ApiKeys.list()
121
+ ```
122
+
123
+ ### 5. Validação de Webhooks HMAC
124
+
125
+ ```python
126
+ from mailifica import Webhooks
127
+
128
+ is_valid = Webhooks.verify_signature(
129
+ payload=raw_body,
130
+ signature=headers.get("mailifica-signature"),
131
+ secret="seu_webhook_secret"
132
+ )
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 📄 Licença
138
+
139
+ MIT © [Mailifica](https://mailifica.com)
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ mailifica/__init__.py
6
+ mailifica/_client.py
7
+ mailifica/_version.py
8
+ mailifica/api_keys.py
9
+ mailifica/batch.py
10
+ mailifica/domains.py
11
+ mailifica/emails.py
12
+ mailifica/errors.py
13
+ mailifica/py.typed
14
+ mailifica/webhooks.py
15
+ mailifica.egg-info/PKG-INFO
16
+ mailifica.egg-info/SOURCES.txt
17
+ mailifica.egg-info/dependency_links.txt
18
+ mailifica.egg-info/requires.txt
19
+ mailifica.egg-info/top_level.txt
20
+ tests/test_mailifica.py
@@ -0,0 +1,4 @@
1
+ requests>=2.28.0
2
+
3
+ [:python_version < "3.10"]
4
+ typing-extensions>=4.0.0
@@ -0,0 +1 @@
1
+ mailifica
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mailifica"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for Mailifica Email Infrastructure"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Mailifica", email = "team@mailifica.com" }
12
+ ]
13
+ license = { text = "MIT" }
14
+ requires-python = ">=3.8"
15
+ dependencies = [
16
+ "requests>=2.28.0",
17
+ "typing-extensions>=4.0.0;python_version<'3.10'",
18
+ ]
19
+ keywords = ["email", "transactional-email", "mailifica", "sdk", "api"]
20
+ classifiers = [
21
+ "Development Status :: 5 - Production/Stable",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Operating System :: OS Independent",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://mailifica.com"
34
+ Documentation = "https://mailifica.com/docs"
35
+ Repository = "https://github.com/mailifica/mailifica-python"
36
+ "Bug Tracker" = "https://github.com/mailifica/mailifica-python/issues"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["mailifica*"]
41
+
42
+ [tool.setuptools.package-data]
43
+ mailifica = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="mailifica",
5
+ version="1.0.0",
6
+ description="Official Python SDK for Mailifica Email Infrastructure",
7
+ long_description=open("README.md", "r", encoding="utf-8").read(),
8
+ long_description_content_type="text/markdown",
9
+ author="Mailifica",
10
+ author_email="team@mailifica.com",
11
+ url="https://github.com/mailifica/mailifica-python",
12
+ packages=find_packages(),
13
+ package_data={"mailifica": ["py.typed"]},
14
+ install_requires=[
15
+ "requests>=2.28.0",
16
+ "typing-extensions>=4.0.0;python_version<'3.10'",
17
+ ],
18
+ python_requires=">=3.8",
19
+ classifiers=[
20
+ "Programming Language :: Python :: 3",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ ],
24
+ )
@@ -0,0 +1,93 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+ import hmac
4
+ import hashlib
5
+ import json
6
+ import mailifica
7
+ from mailifica import Mailifica, Webhooks, AuthenticationError, InvalidRequestError
8
+
9
+ class TestMailificaPython(unittest.TestCase):
10
+ def setUp(self):
11
+ mailifica.api_key = "ma_test_123456789"
12
+ self.client = Mailifica("ma_test_123456789")
13
+
14
+ @patch("requests.request")
15
+ def test_send_email_static(self, mock_request):
16
+ mock_response = MagicMock()
17
+ mock_response.ok = True
18
+ mock_response.status_code = 200
19
+ mock_response.json.return_value = {"id": "email_123", "status": "queued"}
20
+ mock_request.return_value = mock_response
21
+
22
+ response = mailifica.Emails.send({
23
+ "from": "onboarding@empresa.ao",
24
+ "to": "cliente@gmail.com",
25
+ "subject": "Boas-vindas",
26
+ "html": "<p>Olá!</p>"
27
+ })
28
+
29
+ self.assertEqual(response["id"], "email_123")
30
+ mock_request.assert_called_once()
31
+ args, kwargs = mock_request.call_args
32
+ self.assertEqual(kwargs["json"]["from"], "onboarding@empresa.ao")
33
+ self.assertEqual(kwargs["headers"]["Authorization"], "Bearer ma_test_123456789")
34
+
35
+ @patch("requests.request")
36
+ def test_send_email_instance(self, mock_request):
37
+ mock_response = MagicMock()
38
+ mock_response.ok = True
39
+ mock_response.status_code = 200
40
+ mock_response.json.return_value = {"id": "email_456", "status": "sent"}
41
+ mock_request.return_value = mock_response
42
+
43
+ response = self.client.emails.send({
44
+ "from": "onboarding@empresa.ao",
45
+ "to": ["cliente@gmail.com"],
46
+ "subject": "Teste",
47
+ "html": "<p>Teste</p>"
48
+ })
49
+
50
+ self.assertEqual(response["id"], "email_456")
51
+
52
+ @patch("requests.request")
53
+ def test_batch_send(self, mock_request):
54
+ mock_response = MagicMock()
55
+ mock_response.ok = True
56
+ mock_response.status_code = 200
57
+ mock_response.json.return_value = {"data": [{"id": "1"}, {"id": "2"}]}
58
+ mock_request.return_value = mock_response
59
+
60
+ response = mailifica.Batch.send([
61
+ {"from": "a@a.ao", "to": "b@b.com", "subject": "1", "html": "1"},
62
+ {"from": "a@a.ao", "to": "c@c.com", "subject": "2", "html": "2"}
63
+ ])
64
+
65
+ self.assertEqual(len(response["data"]), 2)
66
+
67
+ def test_webhook_verification(self):
68
+ secret = "whsec_test_123"
69
+ payload = json.dumps({"id": "evt_1", "type": "email.delivered"})
70
+ signature = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
71
+
72
+ is_valid = Webhooks.verify_signature(payload, signature, secret)
73
+ self.assertTrue(is_valid)
74
+
75
+ event = Webhooks.construct_event(payload, signature, secret)
76
+ self.assertEqual(event["id"], "evt_1")
77
+
78
+ is_invalid = Webhooks.verify_signature(payload, "bad_signature", secret)
79
+ self.assertFalse(is_invalid)
80
+
81
+ @patch("requests.request")
82
+ def test_error_handling(self, mock_request):
83
+ mock_response = MagicMock()
84
+ mock_response.ok = False
85
+ mock_response.status_code = 401
86
+ mock_response.json.return_value = {"error": {"message": "Invalid API Key", "code": "unauthorized"}}
87
+ mock_request.return_value = mock_response
88
+
89
+ with self.assertRaises(AuthenticationError):
90
+ mailifica.Emails.send({"from": "a@a.ao", "to": "b@b.ao", "subject": "a", "html": "a"})
91
+
92
+ if __name__ == "__main__":
93
+ unittest.main()