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.
Files changed (29) hide show
  1. mailbridge-0.1.0/LICENSE +21 -0
  2. mailbridge-0.1.0/PKG-INFO +136 -0
  3. mailbridge-0.1.0/README.md +101 -0
  4. mailbridge-0.1.0/mailbridge/__init__.py +1 -0
  5. mailbridge-0.1.0/mailbridge/mail.py +21 -0
  6. mailbridge-0.1.0/mailbridge/mailer_factory.py +53 -0
  7. mailbridge-0.1.0/mailbridge/providers/__init__.py +0 -0
  8. mailbridge-0.1.0/mailbridge/providers/brevo_provider.py +25 -0
  9. mailbridge-0.1.0/mailbridge/providers/mailgun_provider.py +23 -0
  10. mailbridge-0.1.0/mailbridge/providers/postmark_provider.py +25 -0
  11. mailbridge-0.1.0/mailbridge/providers/provider_interface.py +7 -0
  12. mailbridge-0.1.0/mailbridge/providers/sendgrid_provider.py +24 -0
  13. mailbridge-0.1.0/mailbridge/providers/ses_provider.py +22 -0
  14. mailbridge-0.1.0/mailbridge/providers/smtp_provider.py +39 -0
  15. mailbridge-0.1.0/mailbridge.egg-info/PKG-INFO +136 -0
  16. mailbridge-0.1.0/mailbridge.egg-info/SOURCES.txt +27 -0
  17. mailbridge-0.1.0/mailbridge.egg-info/dependency_links.txt +1 -0
  18. mailbridge-0.1.0/mailbridge.egg-info/requires.txt +24 -0
  19. mailbridge-0.1.0/mailbridge.egg-info/top_level.txt +1 -0
  20. mailbridge-0.1.0/pyproject.toml +40 -0
  21. mailbridge-0.1.0/setup.cfg +4 -0
  22. mailbridge-0.1.0/tests/test_brevo_provider.py +20 -0
  23. mailbridge-0.1.0/tests/test_mail_facade.py +65 -0
  24. mailbridge-0.1.0/tests/test_mailer_factory.py +97 -0
  25. mailbridge-0.1.0/tests/test_mailgun_provider.py +23 -0
  26. mailbridge-0.1.0/tests/test_postmark_provider.py +20 -0
  27. mailbridge-0.1.0/tests/test_sendgrid_provider.py +23 -0
  28. mailbridge-0.1.0/tests/test_ses_provider.py +17 -0
  29. mailbridge-0.1.0/tests/test_smtp_provider.py +27 -0
@@ -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,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class ProviderInterface(ABC):
4
+ @abstractmethod
5
+ def send(self, to:str, subject: str, body: str, from_email: str = None):
6
+ """Send an email through this provider."""
7
+ pass
@@ -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,24 @@
1
+ requests>=2.26.0
2
+ boto3>=1.20.0
3
+ python-dotenv>=1.0.0
4
+
5
+ [all]
6
+ requests
7
+ boto3
8
+
9
+ [brevo]
10
+ requests
11
+
12
+ [mailgun]
13
+ requests
14
+
15
+ [postmark]
16
+ requests
17
+
18
+ [sendgrid]
19
+ requests
20
+
21
+ [ses]
22
+ boto3
23
+
24
+ [smtp]
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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")