mailbridge 0.1.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.
- mailbridge-0.1.0/LICENSE +21 -0
- mailbridge-0.1.0/PKG-INFO +136 -0
- mailbridge-0.1.0/README.md +101 -0
- mailbridge-0.1.0/mailbridge/__init__.py +1 -0
- mailbridge-0.1.0/mailbridge/mail.py +21 -0
- mailbridge-0.1.0/mailbridge/mailer_factory.py +53 -0
- mailbridge-0.1.0/mailbridge/providers/__init__.py +0 -0
- mailbridge-0.1.0/mailbridge/providers/brevo_provider.py +25 -0
- mailbridge-0.1.0/mailbridge/providers/mailgun_provider.py +23 -0
- mailbridge-0.1.0/mailbridge/providers/postmark_provider.py +25 -0
- mailbridge-0.1.0/mailbridge/providers/provider_interface.py +7 -0
- mailbridge-0.1.0/mailbridge/providers/sendgrid_provider.py +24 -0
- mailbridge-0.1.0/mailbridge/providers/ses_provider.py +22 -0
- mailbridge-0.1.0/mailbridge/providers/smtp_provider.py +39 -0
- mailbridge-0.1.0/mailbridge.egg-info/PKG-INFO +136 -0
- mailbridge-0.1.0/mailbridge.egg-info/SOURCES.txt +27 -0
- mailbridge-0.1.0/mailbridge.egg-info/dependency_links.txt +1 -0
- mailbridge-0.1.0/mailbridge.egg-info/requires.txt +24 -0
- mailbridge-0.1.0/mailbridge.egg-info/top_level.txt +1 -0
- mailbridge-0.1.0/pyproject.toml +40 -0
- mailbridge-0.1.0/setup.cfg +4 -0
- mailbridge-0.1.0/tests/test_brevo_provider.py +20 -0
- mailbridge-0.1.0/tests/test_mail_facade.py +65 -0
- mailbridge-0.1.0/tests/test_mailer_factory.py +97 -0
- mailbridge-0.1.0/tests/test_mailgun_provider.py +23 -0
- mailbridge-0.1.0/tests/test_postmark_provider.py +20 -0
- mailbridge-0.1.0/tests/test_sendgrid_provider.py +23 -0
- mailbridge-0.1.0/tests/test_ses_provider.py +17 -0
- mailbridge-0.1.0/tests/test_smtp_provider.py +27 -0
mailbridge-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 radomirbrkovic
|
|
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,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mailbridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flexible mail delivery library supporting multiple providers (SMTP, SendGrid, Mailgun, SES, Postmark, Brevo)
|
|
5
|
+
Author-email: Radomir Brković <brkovic.radomir@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/radomirbrkovic/mailbridge
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/radomirbrkovic/mailbridge/issues
|
|
9
|
+
Keywords: email,smtp,mailgun,sendgrid,aws,ses,postmark,brevo
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.26.0
|
|
18
|
+
Requires-Dist: boto3>=1.20.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
20
|
+
Provides-Extra: smtp
|
|
21
|
+
Provides-Extra: sendgrid
|
|
22
|
+
Requires-Dist: requests; extra == "sendgrid"
|
|
23
|
+
Provides-Extra: mailgun
|
|
24
|
+
Requires-Dist: requests; extra == "mailgun"
|
|
25
|
+
Provides-Extra: ses
|
|
26
|
+
Requires-Dist: boto3; extra == "ses"
|
|
27
|
+
Provides-Extra: postmark
|
|
28
|
+
Requires-Dist: requests; extra == "postmark"
|
|
29
|
+
Provides-Extra: brevo
|
|
30
|
+
Requires-Dist: requests; extra == "brevo"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: requests; extra == "all"
|
|
33
|
+
Requires-Dist: boto3; extra == "all"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# 📧 MailBridge
|
|
37
|
+
|
|
38
|
+
**MailBridge** is a flexible Python library for sending emails, allowing you to use multiple providers through a single, simple interface.
|
|
39
|
+
It supports **SMTP**, **SendGrid**, **Mailgun**, **Amazon SES**, **Postmark**, and **Brevo**.
|
|
40
|
+
|
|
41
|
+
The package uses the **Facade pattern**, so clients only need to call one method to send an email, while the provider implementation can be swapped via configuration.
|
|
42
|
+
|
|
43
|
+
## ⚡ Features
|
|
44
|
+
- Unified API for all email providers (`Mail.send(...)`)
|
|
45
|
+
- Support for: SMTP, SendGrid, Mailgun, Amazon SES, Postmark, Brevo
|
|
46
|
+
- Configurable via `.env` file
|
|
47
|
+
- Easy integration into any Python project
|
|
48
|
+
- Selective provider installation
|
|
49
|
+
|
|
50
|
+
## 🛠️ Tech Stack
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- `requests` for HTTP providers
|
|
54
|
+
- `boto3` for AWS SES
|
|
55
|
+
- Standard library `smtplib` for SMTP
|
|
56
|
+
|
|
57
|
+
## 📦 Installation
|
|
58
|
+
|
|
59
|
+
Install only the providers you need using **extras**:
|
|
60
|
+
```
|
|
61
|
+
# Only SMTP
|
|
62
|
+
pip install mailbridge[smtp]
|
|
63
|
+
|
|
64
|
+
# Only SES
|
|
65
|
+
pip install mailbridge[ses]
|
|
66
|
+
|
|
67
|
+
# Only SendGrid
|
|
68
|
+
pip install mailbridge[sendgrid]
|
|
69
|
+
|
|
70
|
+
# All providers
|
|
71
|
+
pip install mailbridge[all]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## ⚙️ Configuration
|
|
75
|
+
|
|
76
|
+
Create a `.env` file and define the variables for your chosen provider:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
# Example for SMTP
|
|
80
|
+
MAIL_MAILER=smtp
|
|
81
|
+
MAIL_HOST=smtp.mailserver.com
|
|
82
|
+
MAIL_PORT=587
|
|
83
|
+
MAIL_USERNAME=your_username
|
|
84
|
+
MAIL_PASSWORD=your_password
|
|
85
|
+
MAIL_TLS_ENCRYPTION=True
|
|
86
|
+
MAIL_SSL_ENCRYPTION=False
|
|
87
|
+
|
|
88
|
+
# Example for SendGrid
|
|
89
|
+
MAIL_MAILER=sendgrid
|
|
90
|
+
MAIL_API_KEY=your_sendgrid_api_key
|
|
91
|
+
MAIL_ENDPOINT=https://api.sendgrid.com/v3/mail/send
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 🚀 Usage
|
|
95
|
+
### Sending an email:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
from mailbridge import Mail
|
|
99
|
+
|
|
100
|
+
Mail.send(
|
|
101
|
+
to="user@example.com",
|
|
102
|
+
subject="Welcome!",
|
|
103
|
+
body="<h1>Hello from MailBridge!</h1>",
|
|
104
|
+
from_email="no-reply@example.com"
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Dynamically choosing a provider from `.env`:
|
|
109
|
+
- Change `MAIL_MAILER` in `.env` to `"smtp"`, `"sendgrid"`, `"mailgun"`, `"ses"`, `"postmark"` or `"brevo"`.
|
|
110
|
+
- MailBridge will automatically use the corresponding provider
|
|
111
|
+
|
|
112
|
+
## 📂 Project Structure
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
mailbridge/
|
|
116
|
+
├── mailbridge/
|
|
117
|
+
│ ├── __init__.py
|
|
118
|
+
│ ├── mail.py # Facade
|
|
119
|
+
│ ├── mailer_factory.py # MailerFactory
|
|
120
|
+
│ └── providers/
|
|
121
|
+
│ ├── provider_interface.py
|
|
122
|
+
│ ├── smtp_provider.py
|
|
123
|
+
│ ├── sendgrid_provider.py
|
|
124
|
+
│ ├── mailgun_provider.py
|
|
125
|
+
│ ├── ses_provider.py
|
|
126
|
+
│ ├── postmark_provider.py
|
|
127
|
+
│ └── brevo_provider.py
|
|
128
|
+
├── tests/
|
|
129
|
+
├── setup.py
|
|
130
|
+
├── pyproject.toml
|
|
131
|
+
└── README.md
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 📝 License
|
|
135
|
+
|
|
136
|
+
This project is licensed under the [MIT License](https://opensource.org/license/MIT).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 📧 MailBridge
|
|
2
|
+
|
|
3
|
+
**MailBridge** is a flexible Python library for sending emails, allowing you to use multiple providers through a single, simple interface.
|
|
4
|
+
It supports **SMTP**, **SendGrid**, **Mailgun**, **Amazon SES**, **Postmark**, and **Brevo**.
|
|
5
|
+
|
|
6
|
+
The package uses the **Facade pattern**, so clients only need to call one method to send an email, while the provider implementation can be swapped via configuration.
|
|
7
|
+
|
|
8
|
+
## ⚡ Features
|
|
9
|
+
- Unified API for all email providers (`Mail.send(...)`)
|
|
10
|
+
- Support for: SMTP, SendGrid, Mailgun, Amazon SES, Postmark, Brevo
|
|
11
|
+
- Configurable via `.env` file
|
|
12
|
+
- Easy integration into any Python project
|
|
13
|
+
- Selective provider installation
|
|
14
|
+
|
|
15
|
+
## 🛠️ Tech Stack
|
|
16
|
+
|
|
17
|
+
- Python 3.10+
|
|
18
|
+
- `requests` for HTTP providers
|
|
19
|
+
- `boto3` for AWS SES
|
|
20
|
+
- Standard library `smtplib` for SMTP
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
Install only the providers you need using **extras**:
|
|
25
|
+
```
|
|
26
|
+
# Only SMTP
|
|
27
|
+
pip install mailbridge[smtp]
|
|
28
|
+
|
|
29
|
+
# Only SES
|
|
30
|
+
pip install mailbridge[ses]
|
|
31
|
+
|
|
32
|
+
# Only SendGrid
|
|
33
|
+
pip install mailbridge[sendgrid]
|
|
34
|
+
|
|
35
|
+
# All providers
|
|
36
|
+
pip install mailbridge[all]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ⚙️ Configuration
|
|
40
|
+
|
|
41
|
+
Create a `.env` file and define the variables for your chosen provider:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
# Example for SMTP
|
|
45
|
+
MAIL_MAILER=smtp
|
|
46
|
+
MAIL_HOST=smtp.mailserver.com
|
|
47
|
+
MAIL_PORT=587
|
|
48
|
+
MAIL_USERNAME=your_username
|
|
49
|
+
MAIL_PASSWORD=your_password
|
|
50
|
+
MAIL_TLS_ENCRYPTION=True
|
|
51
|
+
MAIL_SSL_ENCRYPTION=False
|
|
52
|
+
|
|
53
|
+
# Example for SendGrid
|
|
54
|
+
MAIL_MAILER=sendgrid
|
|
55
|
+
MAIL_API_KEY=your_sendgrid_api_key
|
|
56
|
+
MAIL_ENDPOINT=https://api.sendgrid.com/v3/mail/send
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 🚀 Usage
|
|
60
|
+
### Sending an email:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
from mailbridge import Mail
|
|
64
|
+
|
|
65
|
+
Mail.send(
|
|
66
|
+
to="user@example.com",
|
|
67
|
+
subject="Welcome!",
|
|
68
|
+
body="<h1>Hello from MailBridge!</h1>",
|
|
69
|
+
from_email="no-reply@example.com"
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Dynamically choosing a provider from `.env`:
|
|
74
|
+
- Change `MAIL_MAILER` in `.env` to `"smtp"`, `"sendgrid"`, `"mailgun"`, `"ses"`, `"postmark"` or `"brevo"`.
|
|
75
|
+
- MailBridge will automatically use the corresponding provider
|
|
76
|
+
|
|
77
|
+
## 📂 Project Structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
mailbridge/
|
|
81
|
+
├── mailbridge/
|
|
82
|
+
│ ├── __init__.py
|
|
83
|
+
│ ├── mail.py # Facade
|
|
84
|
+
│ ├── mailer_factory.py # MailerFactory
|
|
85
|
+
│ └── providers/
|
|
86
|
+
│ ├── provider_interface.py
|
|
87
|
+
│ ├── smtp_provider.py
|
|
88
|
+
│ ├── sendgrid_provider.py
|
|
89
|
+
│ ├── mailgun_provider.py
|
|
90
|
+
│ ├── ses_provider.py
|
|
91
|
+
│ ├── postmark_provider.py
|
|
92
|
+
│ └── brevo_provider.py
|
|
93
|
+
├── tests/
|
|
94
|
+
├── setup.py
|
|
95
|
+
├── pyproject.toml
|
|
96
|
+
└── README.md
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 📝 License
|
|
100
|
+
|
|
101
|
+
This project is licensed under the [MIT License](https://opensource.org/license/MIT).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .mailer_factory import MailerFactory
|
|
2
|
+
|
|
3
|
+
class Mail:
|
|
4
|
+
@staticmethod
|
|
5
|
+
def send(to: str, subject: str, body: str, from_email: str = None) -> bool:
|
|
6
|
+
"""
|
|
7
|
+
Sends an email using the configured provider from environment variables.
|
|
8
|
+
|
|
9
|
+
:param to: Recipient email
|
|
10
|
+
:param subject: Email subject
|
|
11
|
+
:param body: HTML body of the email
|
|
12
|
+
:param from_email: Optional sender email; if None, provider default is used
|
|
13
|
+
:return: True if email was sent successfully, False otherwise
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
provider = MailerFactory.get_provider()
|
|
17
|
+
return provider.send(to=to, subject=subject, body=body, from_email=from_email)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
# Optional: log error (e.g., with logging library)
|
|
20
|
+
print(f"[MailBridge] Error sending email: {e}")
|
|
21
|
+
return False
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .providers.smtp_provider import SMTPProvider
|
|
3
|
+
from .providers.sendgrid_provider import SendGridProvider
|
|
4
|
+
from .providers.mailgun_provider import MailgunProvider
|
|
5
|
+
from .providers.ses_provider import SESProvider
|
|
6
|
+
from .providers.postmark_provider import PostmarkProvider
|
|
7
|
+
from .providers.brevo_provider import BrevoProvider
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
class MailerFactory:
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_provider():
|
|
15
|
+
provider_name = os.getenv("MAIL_MAILER", "smtp").lower()
|
|
16
|
+
|
|
17
|
+
if provider_name == "smtp":
|
|
18
|
+
return SMTPProvider(
|
|
19
|
+
host=os.getenv("MAIL_HOST"),
|
|
20
|
+
port=int(os.getenv("MAIL_PORT", 587)),
|
|
21
|
+
username=os.getenv("MAIL_USERNAME"),
|
|
22
|
+
password=os.getenv("MAIL_PASSWORD"),
|
|
23
|
+
use_tls=os.getenv("MAIL_TLS_ENCRYPTION", "True") == "True",
|
|
24
|
+
use_ssl=os.getenv("MAIL_SSL_ENCRYPTION", "False") == "True",
|
|
25
|
+
)
|
|
26
|
+
elif provider_name == "sendgrid":
|
|
27
|
+
return SendGridProvider(
|
|
28
|
+
api_key=os.getenv("MAIL_API_KEY"),
|
|
29
|
+
endpoint=os.getenv("MAIL_ENDPOINT"),
|
|
30
|
+
)
|
|
31
|
+
elif provider_name == "mailgun":
|
|
32
|
+
return MailgunProvider(
|
|
33
|
+
api_key=os.getenv("MAIL_API_KEY"),
|
|
34
|
+
endpoint=os.getenv("MAIL_ENDPOINT"),
|
|
35
|
+
)
|
|
36
|
+
elif provider_name == "ses":
|
|
37
|
+
return SESProvider(
|
|
38
|
+
aws_access_key_id=os.getenv("MAIL_AWS_ACCESS_KEY_ID"),
|
|
39
|
+
aws_secret_access_key=os.getenv("MAIL_AWS_SECRET_ACCESS_KEY"),
|
|
40
|
+
region_name=os.getenv("MAIL_AWS_REGION"),
|
|
41
|
+
)
|
|
42
|
+
elif provider_name == "postmark":
|
|
43
|
+
return PostmarkProvider(
|
|
44
|
+
server_token=os.getenv("MAIL_API_KEY"),
|
|
45
|
+
endpoint=os.getenv("MAIL_ENDPOINT"),
|
|
46
|
+
)
|
|
47
|
+
elif provider_name == "brevo":
|
|
48
|
+
return BrevoProvider(
|
|
49
|
+
api_key=os.getenv("MAIL_API_KEY"),
|
|
50
|
+
endpoint=os.getenv("MAIL_ENDPOINT"),
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"Unsupported mail provider: {provider_name}")
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .provider_interface import ProviderInterface
|
|
3
|
+
|
|
4
|
+
class BrevoProvider(ProviderInterface):
|
|
5
|
+
def __init__(self, api_key, endpoint):
|
|
6
|
+
self.api_key = api_key
|
|
7
|
+
self.endpoint = endpoint
|
|
8
|
+
|
|
9
|
+
def send(self, to, subject, body, from_email=None):
|
|
10
|
+
data = {
|
|
11
|
+
"sender": {"email": from_email},
|
|
12
|
+
"to": [{"email": to}],
|
|
13
|
+
"subject": subject,
|
|
14
|
+
"htmlContent": body,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
headers = {
|
|
18
|
+
"api-key": self.api_key,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Accept": "application/json"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resp = requests.post(self.endpoint, json=data, headers=headers)
|
|
24
|
+
resp.raise_for_status()
|
|
25
|
+
return resp.status_code in (200, 201, 202)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .provider_interface import ProviderInterface
|
|
3
|
+
|
|
4
|
+
class MailgunProvider(ProviderInterface):
|
|
5
|
+
def __init__(self, api_key, endpoint):
|
|
6
|
+
self.api_key = api_key
|
|
7
|
+
self.endpoint = endpoint
|
|
8
|
+
|
|
9
|
+
def send(self, to, subject, body, from_email=None):
|
|
10
|
+
data = {
|
|
11
|
+
"from": from_email,
|
|
12
|
+
"to": [to],
|
|
13
|
+
"subject": subject,
|
|
14
|
+
"html": body,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resp = requests.post(
|
|
18
|
+
self.endpoint,
|
|
19
|
+
auth=("api", self.api_key),
|
|
20
|
+
data=data,
|
|
21
|
+
)
|
|
22
|
+
resp.raise_for_status()
|
|
23
|
+
return resp.status_code == 200
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .provider_interface import ProviderInterface
|
|
3
|
+
|
|
4
|
+
class PostmarkProvider(ProviderInterface):
|
|
5
|
+
def __init__(self, server_token, endpoint):
|
|
6
|
+
self.server_token = server_token
|
|
7
|
+
self.endpoint = endpoint
|
|
8
|
+
|
|
9
|
+
def send(self, to, subject, body, from_email=None):
|
|
10
|
+
data = {
|
|
11
|
+
"From": from_email,
|
|
12
|
+
"To": to,
|
|
13
|
+
"Subject": subject,
|
|
14
|
+
"HtmlBody": body,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
headers = {
|
|
18
|
+
"Accept": "application/json",
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"X-Postmark-Server-Token": self.server_token,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resp = requests.post(self.endpoint, json=data, headers=headers)
|
|
24
|
+
resp.raise_for_status()
|
|
25
|
+
return resp.status_code == 200
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .provider_interface import ProviderInterface
|
|
3
|
+
|
|
4
|
+
class SendGridProvider(ProviderInterface):
|
|
5
|
+
def __init__(self, api_key, endpoint):
|
|
6
|
+
self.api_key = api_key
|
|
7
|
+
self.endpoint = endpoint
|
|
8
|
+
|
|
9
|
+
def send(self, to:str, subject: str, body: str, from_email: str = None):
|
|
10
|
+
data = {
|
|
11
|
+
"personalizations": [{"to": [{"email": to}]}],
|
|
12
|
+
"from": {"email": from_email},
|
|
13
|
+
"subject": subject,
|
|
14
|
+
"content": [{"type": "text/html", "value": body}],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
headers = {
|
|
18
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resp = requests.post(self.endpoint, json=data, headers=headers)
|
|
23
|
+
resp.raise_for_status()
|
|
24
|
+
return resp.status_code == 202
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
from .provider_interface import ProviderInterface
|
|
3
|
+
|
|
4
|
+
class SESProvider(ProviderInterface):
|
|
5
|
+
def __init__(self, aws_access_key_id, aws_secret_access_key, region_name):
|
|
6
|
+
self.client = boto3.client(
|
|
7
|
+
"ses",
|
|
8
|
+
aws_access_key_id=aws_access_key_id,
|
|
9
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
10
|
+
region_name=region_name,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def send(self, to, subject, body, from_email=None):
|
|
14
|
+
resp = self.client.send_email(
|
|
15
|
+
Source=from_email,
|
|
16
|
+
Destination={"ToAddresses": [to]},
|
|
17
|
+
Message={
|
|
18
|
+
"Subject": {"Data": subject},
|
|
19
|
+
"Body": {"Html": {"Data": body}},
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
return resp["ResponseMetadata"]["HTTPStatusCode"] == 200
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import smtplib
|
|
2
|
+
from email.mime.text import MIMEText
|
|
3
|
+
from .provider_interface import ProviderInterface
|
|
4
|
+
|
|
5
|
+
class SMTPProvider(ProviderInterface):
|
|
6
|
+
def __init__(self, host: str, port: int, username: str, password: str, use_tls: bool = True, use_ssl: bool = False):
|
|
7
|
+
self.host = host
|
|
8
|
+
self.port = port
|
|
9
|
+
self.username = username
|
|
10
|
+
self.password = password
|
|
11
|
+
self.use_tls = use_tls
|
|
12
|
+
self.use_ssl = use_ssl
|
|
13
|
+
|
|
14
|
+
def send(self, to: str, subject: str, body: str, from_email: str = None) -> bool:
|
|
15
|
+
msg = MIMEText(body, "html")
|
|
16
|
+
msg["Subject"] = subject
|
|
17
|
+
msg["From"] = from_email or self.username
|
|
18
|
+
msg["To"] = to
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
if self.use_ssl:
|
|
22
|
+
# SMTP over SSL (port 465)
|
|
23
|
+
with smtplib.SMTP_SSL(self.host, self.port) as server:
|
|
24
|
+
server.login(self.username, self.password)
|
|
25
|
+
server.send_message(msg)
|
|
26
|
+
else:
|
|
27
|
+
# SMTP with STARTTLS (port 587)
|
|
28
|
+
with smtplib.SMTP(self.host, self.port) as server:
|
|
29
|
+
server.ehlo() # Initialize connection
|
|
30
|
+
if self.use_tls:
|
|
31
|
+
server.starttls()
|
|
32
|
+
server.ehlo()
|
|
33
|
+
if self.username and self.password:
|
|
34
|
+
server.login(self.username, self.password)
|
|
35
|
+
server.send_message(msg)
|
|
36
|
+
return True
|
|
37
|
+
except smtplib.SMTPException as e:
|
|
38
|
+
print(f"[MailBridge] Error sending email: {e}")
|
|
39
|
+
return False
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mailbridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flexible mail delivery library supporting multiple providers (SMTP, SendGrid, Mailgun, SES, Postmark, Brevo)
|
|
5
|
+
Author-email: Radomir Brković <brkovic.radomir@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/radomirbrkovic/mailbridge
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/radomirbrkovic/mailbridge/issues
|
|
9
|
+
Keywords: email,smtp,mailgun,sendgrid,aws,ses,postmark,brevo
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.26.0
|
|
18
|
+
Requires-Dist: boto3>=1.20.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
20
|
+
Provides-Extra: smtp
|
|
21
|
+
Provides-Extra: sendgrid
|
|
22
|
+
Requires-Dist: requests; extra == "sendgrid"
|
|
23
|
+
Provides-Extra: mailgun
|
|
24
|
+
Requires-Dist: requests; extra == "mailgun"
|
|
25
|
+
Provides-Extra: ses
|
|
26
|
+
Requires-Dist: boto3; extra == "ses"
|
|
27
|
+
Provides-Extra: postmark
|
|
28
|
+
Requires-Dist: requests; extra == "postmark"
|
|
29
|
+
Provides-Extra: brevo
|
|
30
|
+
Requires-Dist: requests; extra == "brevo"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: requests; extra == "all"
|
|
33
|
+
Requires-Dist: boto3; extra == "all"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# 📧 MailBridge
|
|
37
|
+
|
|
38
|
+
**MailBridge** is a flexible Python library for sending emails, allowing you to use multiple providers through a single, simple interface.
|
|
39
|
+
It supports **SMTP**, **SendGrid**, **Mailgun**, **Amazon SES**, **Postmark**, and **Brevo**.
|
|
40
|
+
|
|
41
|
+
The package uses the **Facade pattern**, so clients only need to call one method to send an email, while the provider implementation can be swapped via configuration.
|
|
42
|
+
|
|
43
|
+
## ⚡ Features
|
|
44
|
+
- Unified API for all email providers (`Mail.send(...)`)
|
|
45
|
+
- Support for: SMTP, SendGrid, Mailgun, Amazon SES, Postmark, Brevo
|
|
46
|
+
- Configurable via `.env` file
|
|
47
|
+
- Easy integration into any Python project
|
|
48
|
+
- Selective provider installation
|
|
49
|
+
|
|
50
|
+
## 🛠️ Tech Stack
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- `requests` for HTTP providers
|
|
54
|
+
- `boto3` for AWS SES
|
|
55
|
+
- Standard library `smtplib` for SMTP
|
|
56
|
+
|
|
57
|
+
## 📦 Installation
|
|
58
|
+
|
|
59
|
+
Install only the providers you need using **extras**:
|
|
60
|
+
```
|
|
61
|
+
# Only SMTP
|
|
62
|
+
pip install mailbridge[smtp]
|
|
63
|
+
|
|
64
|
+
# Only SES
|
|
65
|
+
pip install mailbridge[ses]
|
|
66
|
+
|
|
67
|
+
# Only SendGrid
|
|
68
|
+
pip install mailbridge[sendgrid]
|
|
69
|
+
|
|
70
|
+
# All providers
|
|
71
|
+
pip install mailbridge[all]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## ⚙️ Configuration
|
|
75
|
+
|
|
76
|
+
Create a `.env` file and define the variables for your chosen provider:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
# Example for SMTP
|
|
80
|
+
MAIL_MAILER=smtp
|
|
81
|
+
MAIL_HOST=smtp.mailserver.com
|
|
82
|
+
MAIL_PORT=587
|
|
83
|
+
MAIL_USERNAME=your_username
|
|
84
|
+
MAIL_PASSWORD=your_password
|
|
85
|
+
MAIL_TLS_ENCRYPTION=True
|
|
86
|
+
MAIL_SSL_ENCRYPTION=False
|
|
87
|
+
|
|
88
|
+
# Example for SendGrid
|
|
89
|
+
MAIL_MAILER=sendgrid
|
|
90
|
+
MAIL_API_KEY=your_sendgrid_api_key
|
|
91
|
+
MAIL_ENDPOINT=https://api.sendgrid.com/v3/mail/send
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 🚀 Usage
|
|
95
|
+
### Sending an email:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
from mailbridge import Mail
|
|
99
|
+
|
|
100
|
+
Mail.send(
|
|
101
|
+
to="user@example.com",
|
|
102
|
+
subject="Welcome!",
|
|
103
|
+
body="<h1>Hello from MailBridge!</h1>",
|
|
104
|
+
from_email="no-reply@example.com"
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Dynamically choosing a provider from `.env`:
|
|
109
|
+
- Change `MAIL_MAILER` in `.env` to `"smtp"`, `"sendgrid"`, `"mailgun"`, `"ses"`, `"postmark"` or `"brevo"`.
|
|
110
|
+
- MailBridge will automatically use the corresponding provider
|
|
111
|
+
|
|
112
|
+
## 📂 Project Structure
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
mailbridge/
|
|
116
|
+
├── mailbridge/
|
|
117
|
+
│ ├── __init__.py
|
|
118
|
+
│ ├── mail.py # Facade
|
|
119
|
+
│ ├── mailer_factory.py # MailerFactory
|
|
120
|
+
│ └── providers/
|
|
121
|
+
│ ├── provider_interface.py
|
|
122
|
+
│ ├── smtp_provider.py
|
|
123
|
+
│ ├── sendgrid_provider.py
|
|
124
|
+
│ ├── mailgun_provider.py
|
|
125
|
+
│ ├── ses_provider.py
|
|
126
|
+
│ ├── postmark_provider.py
|
|
127
|
+
│ └── brevo_provider.py
|
|
128
|
+
├── tests/
|
|
129
|
+
├── setup.py
|
|
130
|
+
├── pyproject.toml
|
|
131
|
+
└── README.md
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 📝 License
|
|
135
|
+
|
|
136
|
+
This project is licensed under the [MIT License](https://opensource.org/license/MIT).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
mailbridge/__init__.py
|
|
5
|
+
mailbridge/mail.py
|
|
6
|
+
mailbridge/mailer_factory.py
|
|
7
|
+
mailbridge.egg-info/PKG-INFO
|
|
8
|
+
mailbridge.egg-info/SOURCES.txt
|
|
9
|
+
mailbridge.egg-info/dependency_links.txt
|
|
10
|
+
mailbridge.egg-info/requires.txt
|
|
11
|
+
mailbridge.egg-info/top_level.txt
|
|
12
|
+
mailbridge/providers/__init__.py
|
|
13
|
+
mailbridge/providers/brevo_provider.py
|
|
14
|
+
mailbridge/providers/mailgun_provider.py
|
|
15
|
+
mailbridge/providers/postmark_provider.py
|
|
16
|
+
mailbridge/providers/provider_interface.py
|
|
17
|
+
mailbridge/providers/sendgrid_provider.py
|
|
18
|
+
mailbridge/providers/ses_provider.py
|
|
19
|
+
mailbridge/providers/smtp_provider.py
|
|
20
|
+
tests/test_brevo_provider.py
|
|
21
|
+
tests/test_mail_facade.py
|
|
22
|
+
tests/test_mailer_factory.py
|
|
23
|
+
tests/test_mailgun_provider.py
|
|
24
|
+
tests/test_postmark_provider.py
|
|
25
|
+
tests/test_sendgrid_provider.py
|
|
26
|
+
tests/test_ses_provider.py
|
|
27
|
+
tests/test_smtp_provider.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mailbridge
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mailbridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Flexible mail delivery library supporting multiple providers (SMTP, SendGrid, Mailgun, SES, Postmark, Brevo)"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Radomir Brković", email = "brkovic.radomir@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
keywords = ["email", "smtp", "mailgun", "sendgrid", "aws", "ses", "postmark", "brevo"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"requests>=2.26.0",
|
|
25
|
+
"boto3>=1.20.0",
|
|
26
|
+
"python-dotenv>=1.0.0"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
smtp = []
|
|
31
|
+
sendgrid = ["requests"]
|
|
32
|
+
mailgun = ["requests"]
|
|
33
|
+
ses = ["boto3"]
|
|
34
|
+
postmark = ["requests"]
|
|
35
|
+
brevo = ["requests"]
|
|
36
|
+
all = ["requests", "boto3"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
"Homepage" = "https://github.com/radomirbrkovic/mailbridge"
|
|
40
|
+
"Bug Tracker" = "https://github.com/radomirbrkovic/mailbridge/issues"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from mailbridge.providers.brevo_provider import BrevoProvider
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def brevo_provider():
|
|
6
|
+
return BrevoProvider(api_key="BREVO.KEY", endpoint="https://api.brevo.com/v3/smtp/email")
|
|
7
|
+
|
|
8
|
+
def test_send_email_brevo(mocker, brevo_provider):
|
|
9
|
+
mock_post = mocker.patch("requests.post")
|
|
10
|
+
mock_response = mocker.Mock(status_code=201)
|
|
11
|
+
mock_post.return_value = mock_response
|
|
12
|
+
|
|
13
|
+
result = brevo_provider.send("to@test.com", "Hi", "<p>Brevo body</p>", "from@test.com")
|
|
14
|
+
|
|
15
|
+
assert result is True
|
|
16
|
+
mock_post.assert_called_once_with(
|
|
17
|
+
"https://api.brevo.com/v3/smtp/email",
|
|
18
|
+
json=mocker.ANY,
|
|
19
|
+
headers=mocker.ANY
|
|
20
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
from mailbridge.mail import Mail
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@patch("mailbridge.mailer_factory.MailerFactory.get_provider")
|
|
7
|
+
def test_mail_send_success(mock_get_provider):
|
|
8
|
+
"""Test that Mail.send() calls the provider's send() method correctly."""
|
|
9
|
+
mock_provider = MagicMock()
|
|
10
|
+
mock_provider.send.return_value = True
|
|
11
|
+
mock_get_provider.return_value = mock_provider
|
|
12
|
+
|
|
13
|
+
result = Mail.send(
|
|
14
|
+
to="user@example.com",
|
|
15
|
+
subject="Welcome",
|
|
16
|
+
body="<h1>Hello!</h1>",
|
|
17
|
+
from_email="noreply@example.com",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Verify result and interactions
|
|
21
|
+
assert result is True
|
|
22
|
+
mock_provider.send.assert_called_once_with(
|
|
23
|
+
to="user@example.com",
|
|
24
|
+
subject="Welcome",
|
|
25
|
+
body="<h1>Hello!</h1>",
|
|
26
|
+
from_email="noreply@example.com",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@patch("mailbridge.mailer_factory.MailerFactory.get_provider")
|
|
30
|
+
def test_mail_send_failure(mock_get_provider):
|
|
31
|
+
"""Test that Mail.send() returns False if provider.send() raises an error."""
|
|
32
|
+
mock_provider = MagicMock()
|
|
33
|
+
mock_provider.send.side_effect = Exception("Something went wrong")
|
|
34
|
+
mock_get_provider.return_value = mock_provider
|
|
35
|
+
|
|
36
|
+
result = Mail.send(
|
|
37
|
+
to="user@example.com",
|
|
38
|
+
subject="Fail Test",
|
|
39
|
+
body="Error expected",
|
|
40
|
+
from_email="noreply@example.com",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert result is False
|
|
44
|
+
mock_provider.send.assert_called_once()
|
|
45
|
+
|
|
46
|
+
@patch("mailbridge.mailer_factory.MailerFactory.get_provider")
|
|
47
|
+
def test_mail_send_without_from_email(mock_get_provider):
|
|
48
|
+
"""Ensure Mail.send works even if from_email is omitted."""
|
|
49
|
+
mock_provider = MagicMock()
|
|
50
|
+
mock_provider.send.return_value = True
|
|
51
|
+
mock_get_provider.return_value = mock_provider
|
|
52
|
+
|
|
53
|
+
result = Mail.send(
|
|
54
|
+
to="user@example.com",
|
|
55
|
+
subject="No sender",
|
|
56
|
+
body="Testing no from_email",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert result is True
|
|
60
|
+
mock_provider.send.assert_called_once_with(
|
|
61
|
+
to="user@example.com",
|
|
62
|
+
subject="No sender",
|
|
63
|
+
body="Testing no from_email",
|
|
64
|
+
from_email=None,
|
|
65
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from mailbridge.mailer_factory import MailerFactory
|
|
4
|
+
from mailbridge.providers.smtp_provider import SMTPProvider
|
|
5
|
+
from mailbridge.providers.sendgrid_provider import SendGridProvider
|
|
6
|
+
from mailbridge.providers.mailgun_provider import MailgunProvider
|
|
7
|
+
from mailbridge.providers.ses_provider import SESProvider
|
|
8
|
+
from mailbridge.providers.postmark_provider import PostmarkProvider
|
|
9
|
+
from mailbridge.providers.brevo_provider import BrevoProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize(
|
|
13
|
+
"provider_name,expected_class,env_vars",
|
|
14
|
+
[
|
|
15
|
+
(
|
|
16
|
+
"smtp",
|
|
17
|
+
SMTPProvider,
|
|
18
|
+
{
|
|
19
|
+
"MAIL_MAILER": "smtp",
|
|
20
|
+
"MAIL_HOST": "smtp.test.com",
|
|
21
|
+
"MAIL_PORT": "587",
|
|
22
|
+
"MAIL_USERNAME": "user@test.com",
|
|
23
|
+
"MAIL_PASSWORD": "secret",
|
|
24
|
+
"MAIL_ENCRYPTION": "True",
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"sendgrid",
|
|
29
|
+
SendGridProvider,
|
|
30
|
+
{
|
|
31
|
+
"MAIL_MAILER": "sendgrid",
|
|
32
|
+
"MAIL_API_KEY": "SG.KEY",
|
|
33
|
+
"MAIL_ENDPOINT": "https://api.sendgrid.com/v3/mail/send",
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"mailgun",
|
|
38
|
+
MailgunProvider,
|
|
39
|
+
{
|
|
40
|
+
"MAIL_MAILER": "mailgun",
|
|
41
|
+
"MAIL_API_KEY": "MG.KEY",
|
|
42
|
+
"MAIL_ENDPOINT": "https://api.mailgun.net/v3/test/messages",
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"ses",
|
|
47
|
+
SESProvider,
|
|
48
|
+
{
|
|
49
|
+
"MAIL_MAILER": "ses",
|
|
50
|
+
"MAIL_AWS_ACCESS_KEY_ID": "AKIAXXX",
|
|
51
|
+
"MAIL_AWS_SECRET_ACCESS_KEY": "SECRET",
|
|
52
|
+
"MAIL_AWS_REGION": "us-east-1",
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"postmark",
|
|
57
|
+
PostmarkProvider,
|
|
58
|
+
{
|
|
59
|
+
"MAIL_MAILER": "postmark",
|
|
60
|
+
"MAIL_API_KEY": "PM.KEY",
|
|
61
|
+
"MAIL_ENDPOINT": "https://api.postmarkapp.com/email",
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"brevo",
|
|
66
|
+
BrevoProvider,
|
|
67
|
+
{
|
|
68
|
+
"MAIL_MAILER": "brevo",
|
|
69
|
+
"MAIL_API_KEY": "BREVO.KEY",
|
|
70
|
+
"MAIL_ENDPOINT": "https://api.brevo.com/v3/smtp/email",
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
def test_mailer_factory_get_provider(provider_name, expected_class, env_vars, mocker):
|
|
76
|
+
# Save original environment
|
|
77
|
+
original_env = os.environ.copy()
|
|
78
|
+
|
|
79
|
+
# Override env vars for test
|
|
80
|
+
os.environ.update(env_vars)
|
|
81
|
+
|
|
82
|
+
provider = MailerFactory.get_provider()
|
|
83
|
+
|
|
84
|
+
assert isinstance(provider, expected_class), f"{provider_name} did not return expected provider"
|
|
85
|
+
|
|
86
|
+
# Restore environment
|
|
87
|
+
os.environ.clear()
|
|
88
|
+
os.environ.update(original_env)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_mailer_factory_invalid_provider(monkeypatch):
|
|
92
|
+
monkeypatch.setenv("MAIL_MAILER", "unknown")
|
|
93
|
+
|
|
94
|
+
with pytest.raises(ValueError) as exc_info:
|
|
95
|
+
MailerFactory.get_provider()
|
|
96
|
+
|
|
97
|
+
assert "Unsupported mail provider" in str(exc_info.value)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from mailbridge.providers.mailgun_provider import MailgunProvider
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def mailgun_provider():
|
|
6
|
+
return MailgunProvider(
|
|
7
|
+
api_key="MG.TESTKEY",
|
|
8
|
+
endpoint="https://api.mailgun.net/v3/test/messages"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def test_send_email_mailgun(mocker, mailgun_provider):
|
|
12
|
+
mock_post = mocker.patch("requests.post")
|
|
13
|
+
mock_response = mocker.Mock(status_code=200)
|
|
14
|
+
mock_post.return_value = mock_response
|
|
15
|
+
|
|
16
|
+
result = mailgun_provider.send("to@test.com", "Hi", "<p>Mailgun body</p>", "from@test.com")
|
|
17
|
+
|
|
18
|
+
assert result is True
|
|
19
|
+
mock_post.assert_called_once_with(
|
|
20
|
+
"https://api.mailgun.net/v3/test/messages",
|
|
21
|
+
auth=("api", "MG.TESTKEY"),
|
|
22
|
+
data=mocker.ANY
|
|
23
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from mailbridge.providers.postmark_provider import PostmarkProvider
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def postmark_provider():
|
|
6
|
+
return PostmarkProvider(server_token="PM.TEST", endpoint="https://api.postmarkapp.com/email")
|
|
7
|
+
|
|
8
|
+
def test_send_email_postmark(mocker, postmark_provider):
|
|
9
|
+
mock_post = mocker.patch("requests.post")
|
|
10
|
+
mock_response = mocker.Mock(status_code=200)
|
|
11
|
+
mock_post.return_value = mock_response
|
|
12
|
+
|
|
13
|
+
result = postmark_provider.send("to@test.com", "Hi", "<p>Postmark body</p>", "from@test.com")
|
|
14
|
+
|
|
15
|
+
assert result is True
|
|
16
|
+
mock_post.assert_called_once_with(
|
|
17
|
+
"https://api.postmarkapp.com/email",
|
|
18
|
+
json=mocker.ANY,
|
|
19
|
+
headers=mocker.ANY
|
|
20
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from mailbridge.providers.sendgrid_provider import SendGridProvider
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def sendgrid_provider():
|
|
6
|
+
return SendGridProvider(
|
|
7
|
+
api_key="SG.TESTKEY",
|
|
8
|
+
endpoint="https://api.sendgrid.com/v3/mail/send"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def test_send_email_sendgrid(mocker, sendgrid_provider):
|
|
12
|
+
mock_post = mocker.patch("requests.post")
|
|
13
|
+
mock_response = mocker.Mock(status_code=202)
|
|
14
|
+
mock_post.return_value = mock_response
|
|
15
|
+
|
|
16
|
+
result = sendgrid_provider.send("to@test.com", "Hello", "<p>Body</p>", "from@test.com")
|
|
17
|
+
|
|
18
|
+
assert result is True
|
|
19
|
+
mock_post.assert_called_once_with(
|
|
20
|
+
"https://api.sendgrid.com/v3/mail/send",
|
|
21
|
+
json=mocker.ANY,
|
|
22
|
+
headers=mocker.ANY
|
|
23
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from mailbridge.providers.ses_provider import SESProvider
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def ses_provider(mocker):
|
|
6
|
+
mock_client = mocker.Mock()
|
|
7
|
+
mock_client.send_email.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}
|
|
8
|
+
|
|
9
|
+
mock_boto3 = mocker.patch("boto3.client", return_value=mock_client)
|
|
10
|
+
provider = SESProvider("AKIAXXX", "SECRET", "us-east-1")
|
|
11
|
+
|
|
12
|
+
return provider
|
|
13
|
+
|
|
14
|
+
def test_send_email_ses(ses_provider):
|
|
15
|
+
result = ses_provider.send("to@test.com", "Subject", "<p>Body</p>", "from@test.com")
|
|
16
|
+
|
|
17
|
+
assert result is True
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from mailbridge.providers.smtp_provider import SMTPProvider
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def smtp_provider():
|
|
6
|
+
return SMTPProvider(
|
|
7
|
+
host="smtp.test.com",
|
|
8
|
+
port=587,
|
|
9
|
+
username="user@test.com",
|
|
10
|
+
password="secret",
|
|
11
|
+
use_tls=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def test_send_email_smtp(mocker, smtp_provider):
|
|
15
|
+
mock_smtp = mocker.patch("smtplib.SMTP", autospec=True)
|
|
16
|
+
instance = mock_smtp.return_value.__enter__.return_value
|
|
17
|
+
|
|
18
|
+
result = smtp_provider.send(
|
|
19
|
+
to="receiver@test.com",
|
|
20
|
+
subject="Test Subject",
|
|
21
|
+
body="<b>Hello SMTP</b>",
|
|
22
|
+
from_email="sender@test.com"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert result is True
|
|
26
|
+
instance.send_message.assert_called_once()
|
|
27
|
+
instance.login.assert_called_once_with("user@test.com", "secret")
|