MailToolsBox 0.1.0.4__tar.gz → 1.0.1__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,6 @@
1
+ from MailToolsBox.mailSender import EmailSender, SendAgent
2
+ from MailToolsBox.imapClient import ImapAgent
3
+
4
+ __version__ = "1.0.1"
5
+
6
+ __all__ = ["EmailSender", "SendAgent", "ImapAgent", "__version__"]
@@ -2,6 +2,7 @@ import imaplib
2
2
  import email
3
3
  import json
4
4
  import datetime
5
+ import os
5
6
  from typing import List
6
7
 
7
8
 
@@ -12,11 +13,46 @@ class ImapAgent:
12
13
  self.server_address = server_address
13
14
  self.mail = None
14
15
 
16
+ @classmethod
17
+ def from_env(cls) -> "ImapAgent":
18
+ """Create :class:`ImapAgent` from environment variables.
19
+
20
+ Required variables are ``IMAP_EMAIL``, ``IMAP_PASSWORD`` and
21
+ ``IMAP_SERVER``.
22
+ """
23
+ email_account = os.environ["IMAP_EMAIL"]
24
+ password = os.environ["IMAP_PASSWORD"]
25
+ server = os.environ["IMAP_SERVER"]
26
+ return cls(email_account, password, server)
27
+
28
+ def __enter__(self) -> "ImapAgent":
29
+ self.login_account()
30
+ return self
31
+
32
+ def __exit__(self, exc_type, exc, tb) -> None:
33
+ self.logout_account()
34
+
15
35
  def login_account(self):
16
36
  self.mail = imaplib.IMAP4_SSL(self.server_address)
17
37
  self.mail.login(self.email_account, self.password)
18
38
 
39
+ def logout_account(self):
40
+ if self.mail is not None:
41
+ try:
42
+ self.mail.logout()
43
+ finally:
44
+ self.mail = None
45
+
19
46
  def download_mail_text(self, path='', mailbox='INBOX'):
47
+ """Download all emails in ``mailbox`` and write them as plain text.
48
+
49
+ This method now ensures that a connection to the IMAP server has been
50
+ established before attempting to access any mailbox. If no connection
51
+ exists, :py:meth:`login_account` is invoked to create one.
52
+ """
53
+ if self.mail is None:
54
+ self.login_account()
55
+
20
56
  with open(f'{path}email.txt', 'w') as f:
21
57
  self.mail.select(mailbox)
22
58
  _, data = self.mail.uid('search', None, 'ALL')
@@ -48,15 +84,14 @@ class ImapAgent:
48
84
  f.write(
49
85
  f"From: {email_from}\nTo: {email_to}\nDate: {local_message_date}\nSubject: {subject}\n\nBody:\n\n{body.decode('utf-8')}\n\n")
50
86
  self.mail.close()
87
+ self.logout_account()
51
88
 
52
- import json
53
89
 
54
90
  def download_mail_json(self, lookup: str = 'ALL', save: bool = False, path: str = '', file_name: str = 'mail.json') -> str:
55
91
  save_json = save
56
- self.login_account()
57
92
 
58
- with imaplib.IMAP4_SSL(self.imap_server) as mail:
59
- mail.login(self.username, self.password)
93
+ with imaplib.IMAP4_SSL(self.server_address) as mail:
94
+ mail.login(self.email_account, self.password)
60
95
  mail.select("inbox")
61
96
  result, data = mail.uid('search', None, lookup) # (ALL/UNSEEN)
62
97
  uids = data[0].split()
@@ -117,3 +152,5 @@ class ImapAgent:
117
152
  file_name = f"{path}email_{i}.msg"
118
153
  with open(file_name, 'w') as f:
119
154
  f.write(email_message.as_string())
155
+ self.mail.close()
156
+ self.logout_account()
@@ -0,0 +1,272 @@
1
+ import os
2
+ import smtplib
3
+ import aiosmtplib
4
+ from email.mime.text import MIMEText
5
+ from email.mime.multipart import MIMEMultipart
6
+ from email.mime.application import MIMEApplication
7
+ from email.utils import COMMASPACE, formatdate
8
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
9
+ from typing import List, Optional, Iterable
10
+ from pathlib import Path
11
+ import logging
12
+ from ssl import create_default_context
13
+ from email_validator import validate_email, EmailNotValidError
14
+
15
+ # Set up logging without configuring the root logger
16
+ logger = logging.getLogger(__name__)
17
+ logger.addHandler(logging.NullHandler())
18
+
19
+
20
+ class EmailSender:
21
+ """Modern email sender with sync/async support and enhanced features."""
22
+
23
+ def __init__(
24
+ self,
25
+ user_email: str,
26
+ server_smtp_address: str,
27
+ user_email_password: str,
28
+ port: int = 587,
29
+ timeout: int = 10,
30
+ validate_emails: bool = True,
31
+ ) -> None:
32
+ self.user_email = self._validate_email(user_email) if validate_emails else user_email
33
+ self.user_email_password = user_email_password
34
+ self.server_smtp_address = server_smtp_address
35
+ self.port = port
36
+ self.timeout = timeout
37
+ self.validate_emails = validate_emails
38
+ template_dir = Path(__file__).resolve().parent / "templates"
39
+ self.template_env = Environment(
40
+ loader=FileSystemLoader(str(template_dir)),
41
+ autoescape=select_autoescape(['html', 'xml'])
42
+ )
43
+ self.ssl_context = create_default_context()
44
+ logger.info("EmailSender initialised for %s", self.user_email)
45
+
46
+ @classmethod
47
+ def from_env(cls) -> "EmailSender":
48
+ """Create :class:`EmailSender` from environment variables.
49
+
50
+ Required variables are ``EMAIL``, ``SMTP_SERVER`` and ``EMAIL_PASSWORD``.
51
+ ``SMTP_PORT`` is optional and defaults to ``587``.
52
+ """
53
+ user_email = os.environ["EMAIL"]
54
+ server = os.environ["SMTP_SERVER"]
55
+ password = os.environ["EMAIL_PASSWORD"]
56
+ port = int(os.getenv("SMTP_PORT", 587))
57
+ return cls(
58
+ user_email=user_email,
59
+ server_smtp_address=server,
60
+ user_email_password=password,
61
+ port=port,
62
+ )
63
+
64
+ def _validate_email(self, email_address: str) -> str:
65
+ """Validate and normalize email address using email-validator."""
66
+ try:
67
+ result = validate_email(email_address, check_deliverability=False)
68
+ return result.normalized
69
+ except EmailNotValidError as e:
70
+ logger.error(f"Invalid email address: {email_address}")
71
+ raise ValueError(f"Invalid email address: {email_address}") from e
72
+
73
+ def _create_base_message(
74
+ self,
75
+ subject: str,
76
+ recipients: Iterable[str],
77
+ cc: Optional[Iterable[str]] = None
78
+ ) -> MIMEMultipart:
79
+ """Create MIME message with proper headers."""
80
+ msg = MIMEMultipart()
81
+ msg['From'] = self.user_email
82
+ msg['Subject'] = subject
83
+ msg['Date'] = formatdate(localtime=True)
84
+
85
+ if self.validate_emails:
86
+ recipients = [self._validate_email(r) for r in recipients]
87
+
88
+ msg['To'] = COMMASPACE.join(recipients)
89
+
90
+ if cc:
91
+ validated_cc = [self._validate_email(c) for c in cc] if self.validate_emails else cc
92
+ msg['Cc'] = COMMASPACE.join(validated_cc)
93
+
94
+ return msg
95
+
96
+ def _add_attachments(self, msg: MIMEMultipart, attachments: Iterable[str]) -> None:
97
+ """Add multiple attachments to the message."""
98
+ for file_path in attachments:
99
+ path = Path(file_path)
100
+ if not path.exists():
101
+ logger.warning(f"Attachment not found: {file_path}")
102
+ continue
103
+
104
+ with open(path, 'rb') as f:
105
+ part = MIMEApplication(
106
+ f.read(),
107
+ Name=path.name
108
+ )
109
+ part['Content-Disposition'] = f'attachment; filename="{path.name}"'
110
+ msg.attach(part)
111
+
112
+
113
+ def send(
114
+ self,
115
+ recipients: Iterable[str],
116
+ subject: str,
117
+ message_body: str,
118
+ cc: Optional[Iterable[str]] = None,
119
+ bcc: Optional[Iterable[str]] = None,
120
+ attachments: Optional[Iterable[str]] = None,
121
+ use_tls: bool = True,
122
+ html: bool = False
123
+ ) -> None:
124
+ """Synchronous email sending with improved error handling."""
125
+ logger.info("Sending email to %s", ", ".join(recipients))
126
+ # Build the MIME message without exposing BCC recipients
127
+ msg = self._create_base_message(subject, recipients, cc)
128
+ validated_bcc = None
129
+ if bcc:
130
+ validated_bcc = [self._validate_email(b) for b in bcc] if self.validate_emails else list(bcc)
131
+ msg.attach(MIMEText(message_body, 'html' if html else 'plain'))
132
+
133
+ if attachments:
134
+ self._add_attachments(msg, attachments)
135
+
136
+ try:
137
+ with smtplib.SMTP(self.server_smtp_address, self.port, timeout=self.timeout) as server:
138
+ if use_tls:
139
+ server.starttls(context=self.ssl_context)
140
+ server.login(self.user_email, self.user_email_password)
141
+ all_recipients = list(recipients)
142
+ if cc:
143
+ all_recipients.extend(cc)
144
+ if validated_bcc:
145
+ all_recipients.extend(validated_bcc)
146
+ server.send_message(msg, to_addrs=all_recipients)
147
+ except smtplib.SMTPException as e:
148
+ logger.error(f"SMTP error occurred: {str(e)}")
149
+ raise
150
+ except Exception as e:
151
+ logger.error(f"Unexpected error: {str(e)}")
152
+ raise
153
+
154
+ def send_bulk(
155
+ self,
156
+ recipients: Iterable[str],
157
+ subject: str,
158
+ message_body: str,
159
+ **kwargs,
160
+ ) -> None:
161
+ """Send the same message to many recipients individually."""
162
+ for recipient in recipients:
163
+ logger.info("Sending bulk email to %s", recipient)
164
+ self.send([recipient], subject, message_body, **kwargs)
165
+
166
+ async def send_async(
167
+ self,
168
+ recipients: Iterable[str],
169
+ subject: str,
170
+ message_body: str,
171
+ cc: Optional[Iterable[str]] = None,
172
+ bcc: Optional[Iterable[str]] = None,
173
+ attachments: Optional[Iterable[str]] = None,
174
+ use_tls: bool = True,
175
+ html: bool = False
176
+ ) -> None:
177
+ """Asynchronous email sending using aiosmtplib."""
178
+ msg = self._create_base_message(subject, recipients, cc)
179
+ validated_bcc = None
180
+ if bcc:
181
+ validated_bcc = [self._validate_email(b) for b in bcc] if self.validate_emails else list(bcc)
182
+ msg.attach(MIMEText(message_body, 'html' if html else 'plain'))
183
+
184
+ if attachments:
185
+ self._add_attachments(msg, attachments)
186
+
187
+ try:
188
+ async with aiosmtplib.SMTP(hostname=self.server_smtp_address, port=self.port, timeout=self.timeout) as server:
189
+ if use_tls:
190
+ await server.starttls(context=self.ssl_context)
191
+ await server.login(self.user_email, self.user_email_password)
192
+ all_recipients = list(recipients)
193
+ if cc:
194
+ all_recipients.extend(cc)
195
+ if validated_bcc:
196
+ all_recipients.extend(validated_bcc)
197
+ await server.send_message(msg, to_addrs=all_recipients)
198
+ except aiosmtplib.errors.SMTPException as e:
199
+ logger.error(f"SMTP error occurred: {str(e)}")
200
+ raise
201
+ except Exception as e:
202
+ logger.error(f"Unexpected error: {str(e)}")
203
+ raise
204
+
205
+ def send_template(
206
+ self,
207
+ recipient: str,
208
+ subject: str,
209
+ template_name: str,
210
+ context: dict,
211
+ cc: Optional[Iterable[str]] = None,
212
+ attachments: Optional[Iterable[str]] = None,
213
+ use_tls: bool = True
214
+ ) -> None:
215
+ """Send email using Jinja2 template with autoescaping."""
216
+ logger.info("Sending templated email to %s using %s", recipient, template_name)
217
+ template = self.template_env.get_template(template_name)
218
+ html_content = template.render(**context)
219
+ self.send([recipient], subject, html_content, cc=cc, attachments=attachments, use_tls=use_tls, html=True)
220
+
221
+
222
+
223
+ # Backward compatibility layer
224
+ class SendAgent(EmailSender):
225
+ """Legacy compatibility layer maintaining original interface."""
226
+
227
+ def send_mail(
228
+ self,
229
+ recipient_email: Optional[List[str]],
230
+ subject: str,
231
+ message_body: str,
232
+ cc: Optional[List[str]] = None,
233
+ bcc: Optional[List[str]] = None,
234
+ attachments: Optional[List[str]] = None,
235
+ tls: bool = True
236
+ ) -> None:
237
+ logger.warning("SendAgent is deprecated, use EmailSender instead")
238
+
239
+ # Convert parameters to new format
240
+ self.send(
241
+ recipients=recipient_email,
242
+ subject=subject,
243
+ message_body=message_body,
244
+ cc=cc,
245
+ bcc=bcc,
246
+ attachments=attachments,
247
+ use_tls=tls
248
+ )
249
+
250
+
251
+ def send_mail_with_template(
252
+ self,
253
+ recipient_email: str,
254
+ subject: str,
255
+ template_path: str,
256
+ template_vars: dict,
257
+ cc: Optional[List[str]] = None,
258
+ attachments: Optional[List[str]] = None,
259
+ tls: bool = True
260
+ ) -> None:
261
+ logger.warning("send_mail_with_template is deprecated, use send_template instead")
262
+
263
+ self.send_template(
264
+ recipient=recipient_email,
265
+ subject=subject,
266
+ template_name=template_path,
267
+ context=template_vars,
268
+ cc=cc,
269
+ attachments=attachments,
270
+ use_tls=tls
271
+ )
272
+
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: MailToolsBox
3
+ Version: 1.0.1
4
+ Summary: A modern and efficient Python library for sending emails with SMTP, Jinja2 templates, and attachments.
5
+ Home-page: https://github.com/rambod/MailToolsBox
6
+ Download-URL: https://github.com/rambod/MailToolsBox/archive/refs/tags/v1.0.0.tar.gz
7
+ Author: Rambod Ghashghai
8
+ Author-email: gh.rambod@gmail.com
9
+ License: MIT
10
+ Keywords: Mail,SMTP,email,tools,attachments,Jinja2,Python,email-validation
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Communications :: Email
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.7
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
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE.txt
25
+ Requires-Dist: Jinja2>=3.0.2
26
+ Requires-Dist: email-validator>=2.0.0
27
+ Requires-Dist: aiosmtplib>=2.0.0
28
+ Dynamic: author
29
+ Dynamic: author-email
30
+ Dynamic: classifier
31
+ Dynamic: description
32
+ Dynamic: description-content-type
33
+ Dynamic: download-url
34
+ Dynamic: home-page
35
+ Dynamic: keywords
36
+ Dynamic: license
37
+ Dynamic: license-file
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+ # MailToolsBox
43
+
44
+ MailToolsBox is a modern, feature-rich Python package designed for sending and managing emails with ease. It provides robust functionality for handling SMTP email sending, template-based emails using Jinja2, attachments, CC/BCC support, and email validation. Additionally, MailToolsBox ensures backward compatibility with legacy implementations.
45
+
46
+ ## Features
47
+
48
+ - **Send emails via SMTP with ease**
49
+ - **Support for multiple recipients (To, CC, BCC)**
50
+ - **HTML and plain text email support**
51
+ - **Attachment handling**
52
+ - **Template-based email rendering using Jinja2**
53
+ - **Secure email transactions with TLS/SSL**
54
+ - **Asynchronous email sending via `send_async`**
55
+ - **Email address validation**
56
+ - **Logging for debugging and monitoring**
57
+ - **Bulk email sending with `send_bulk`**
58
+ - **Convenient configuration via environment variables using `from_env`**
59
+ - **Backward compatibility with `SendAgent`**
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ Install MailToolsBox from PyPI using pip:
66
+
67
+ ```bash
68
+ pip install MailToolsBox
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Getting Started
74
+
75
+ ### 1. **Sending a Basic Email**
76
+
77
+ The `EmailSender` class is the primary interface for sending emails. Below is an example of sending a simple plain text email:
78
+
79
+ ```python
80
+ from MailToolsBox import EmailSender
81
+
82
+ # Email configuration
83
+ sender = EmailSender(
84
+ user_email="your@email.com",
85
+ server_smtp_address="smtp.example.com",
86
+ user_email_password="yourpassword",
87
+ port=587
88
+ )
89
+
90
+ # Sending email
91
+ sender.send(
92
+ recipients=["recipient@example.com"],
93
+ subject="Test Email",
94
+ message_body="Hello, this is a test email!"
95
+ )
96
+ ```
97
+
98
+ ---
99
+
100
+ ### 2. **Sending an HTML Email with Attachments**
101
+
102
+ ```python
103
+ sender.send(
104
+ recipients=["recipient@example.com"],
105
+ subject="HTML Email Example",
106
+ message_body="""<h1>Welcome!</h1><p>This is an <strong>HTML email</strong>.</p>""",
107
+
108
+ html=True,
109
+ attachments=["/path/to/document.pdf"]
110
+ )
111
+ ```
112
+
113
+ ---
114
+
115
+ ### 3. **Using Email Templates (Jinja2)**
116
+
117
+ MailToolsBox allows sending emails using Jinja2 templates stored in the `templates/` directory.
118
+
119
+ #### **Example Template (`templates/welcome.html`)**:
120
+
121
+ ```html
122
+ <html>
123
+ <head>
124
+ <title>Welcome</title>
125
+ </head>
126
+ <body>
127
+ <h1>Welcome, {{ username }}!</h1>
128
+ <p>Click <a href="{{ activation_link }}">here</a> to activate your account.</p>
129
+ </body>
130
+ </html>
131
+ ```
132
+
133
+ #### **Sending an Email with a Template**:
134
+
135
+ ```python
136
+ context = {
137
+ "username": "John Doe",
138
+ "activation_link": "https://example.com/activate"
139
+ }
140
+
141
+ sender.send_template(
142
+ recipient="recipient@example.com",
143
+ subject="Welcome to Our Service",
144
+ template_name="welcome.html",
145
+ context=context
146
+ )
147
+ ```
148
+
149
+ ---
150
+
151
+ ### 4. **CC, BCC, and Multiple Recipients**
152
+
153
+ ```python
154
+ sender.send(
155
+ recipients=["recipient@example.com"],
156
+ subject="CC & BCC Example",
157
+ message_body="This email has CC and BCC recipients!",
158
+ cc=["cc@example.com"],
159
+ bcc=["bcc@example.com"]
160
+ )
161
+ ```
162
+
163
+ ---
164
+
165
+ ### 5. **Asynchronous Email Sending**
166
+
167
+ Send emails without blocking using `send_async` and `asyncio`:
168
+
169
+ ```python
170
+ import asyncio
171
+
172
+ async def main():
173
+ await sender.send_async(
174
+ recipients=["recipient@example.com"],
175
+ subject="Async Example",
176
+ message_body="This email was sent asynchronously!"
177
+ )
178
+
179
+ asyncio.run(main())
180
+ ```
181
+
182
+ ---
183
+
184
+ ### 6. **Bulk Email Sending**
185
+
186
+ Send the same message to many recipients individually, ensuring privacy:
187
+
188
+ ```python
189
+ sender.send_bulk(
190
+ recipients=["user1@example.com", "user2@example.com"],
191
+ subject="Announcement",
192
+ message_body="This email is sent separately to each recipient!",
193
+ )
194
+ ```
195
+
196
+ ---
197
+
198
+ ### 7. **Backward Compatibility with `SendAgent`**
199
+
200
+ For those migrating from earlier versions, `SendAgent` ensures seamless compatibility:
201
+
202
+ ```python
203
+ from MailToolsBox import SendAgent
204
+
205
+ legacy_sender = SendAgent(
206
+ user_email="your@email.com",
207
+ server_smtp_address="smtp.example.com",
208
+ user_email_password="yourpassword",
209
+ port=587
210
+ )
211
+
212
+ legacy_sender.send_mail(
213
+ recipient_email=["recipient@example.com"],
214
+ subject="Legacy Compatibility Test",
215
+ message_body="Testing backward compatibility."
216
+ )
217
+ ```
218
+
219
+ ---
220
+
221
+ ### 8. **Retrieving Emails via IMAP**
222
+
223
+ `ImapAgent` can be used to fetch messages from an IMAP mailbox. It supports
224
+ loading configuration from environment variables with `from_env` and can be
225
+ used as a context manager to ensure connections are closed properly.
226
+
227
+ ```python
228
+ from MailToolsBox import ImapAgent
229
+
230
+ # Using environment variables IMAP_EMAIL, IMAP_PASSWORD and IMAP_SERVER
231
+ agent = ImapAgent.from_env()
232
+
233
+ with agent as imap:
234
+ imap.download_mail_text(path="/tmp/")
235
+ ```
236
+
237
+
238
+ ---
239
+
240
+ ## Configuration & Security Best Practices
241
+
242
+ - **Use environment variables** instead of hardcoding credentials.
243
+ - **Enable 2FA** on your email provider and use app passwords if required.
244
+ - **Use TLS/SSL** to ensure secure email delivery.
245
+
246
+ Example using environment variables:
247
+
248
+ ```python
249
+ sender = EmailSender.from_env()
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Error Handling & Logging
255
+
256
+ MailToolsBox provides built-in logging to help debug issues:
257
+
258
+ ```python
259
+ import logging
260
+ logging.basicConfig(level=logging.INFO)
261
+ ```
262
+
263
+ Example of handling exceptions:
264
+
265
+ ```python
266
+ try:
267
+ sender.send(
268
+ recipients=["recipient@example.com"],
269
+ subject="Error Handling Test",
270
+ message_body="This is a test email."
271
+ )
272
+ except Exception as e:
273
+ print(f"Failed to send email: {e}")
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Contributing
279
+
280
+ MailToolsBox is an open-source project. Contributions are welcome! To contribute:
281
+
282
+ 1. Fork the repository on GitHub.
283
+ 2. Create a new feature branch.
284
+ 3. Implement changes and write tests.
285
+ 4. Submit a pull request for review.
286
+
287
+ For discussions, visit **[rambod.net](https://www.rambod.net)**. **[mailtoolsbox](https://rambod.net/portfolio/mailtoolsbox/)**
288
+
289
+ ---
290
+
291
+ ## License
292
+
293
+ MailToolsBox is licensed under the MIT License. See the [LICENSE](https://choosealicense.com/licenses/mit/) for details.
@@ -8,4 +8,7 @@ MailToolsBox.egg-info/PKG-INFO
8
8
  MailToolsBox.egg-info/SOURCES.txt
9
9
  MailToolsBox.egg-info/dependency_links.txt
10
10
  MailToolsBox.egg-info/requires.txt
11
- MailToolsBox.egg-info/top_level.txt
11
+ MailToolsBox.egg-info/top_level.txt
12
+ tests/__init__.py
13
+ tests/test_imap_agent.py
14
+ tests/test_mail_sender.py
@@ -0,0 +1,3 @@
1
+ Jinja2>=3.0.2
2
+ email-validator>=2.0.0
3
+ aiosmtplib>=2.0.0