MailToolsBox 1.0.0__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.
- mailtoolsbox-1.0.1/MailToolsBox/__init__.py +6 -0
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox/imapClient.py +41 -4
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox/mailSender.py +94 -40
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox.egg-info/PKG-INFO +63 -13
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox.egg-info/SOURCES.txt +4 -1
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox.egg-info/requires.txt +1 -0
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox.egg-info/top_level.txt +1 -0
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/PKG-INFO +63 -13
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/README.md +59 -11
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/setup.py +3 -2
- mailtoolsbox-1.0.1/tests/__init__.py +0 -0
- mailtoolsbox-1.0.1/tests/test_imap_agent.py +146 -0
- mailtoolsbox-1.0.1/tests/test_mail_sender.py +100 -0
- mailtoolsbox-1.0.0/MailToolsBox/__init__.py +0 -2
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/LICENSE.txt +0 -0
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/MailToolsBox.egg-info/dependency_links.txt +0 -0
- {mailtoolsbox-1.0.0 → mailtoolsbox-1.0.1}/setup.cfg +0 -0
|
@@ -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.
|
|
59
|
-
mail.login(self.
|
|
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()
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import smtplib
|
|
3
|
+
import aiosmtplib
|
|
2
4
|
from email.mime.text import MIMEText
|
|
3
5
|
from email.mime.multipart import MIMEMultipart
|
|
4
6
|
from email.mime.application import MIMEApplication
|
|
5
7
|
from email.utils import COMMASPACE, formatdate
|
|
6
|
-
from jinja2 import
|
|
8
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
7
9
|
from typing import List, Optional, Iterable
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
import logging
|
|
10
12
|
from ssl import create_default_context
|
|
11
13
|
from email_validator import validate_email, EmailNotValidError
|
|
12
14
|
|
|
13
|
-
# Set up logging
|
|
15
|
+
# Set up logging without configuring the root logger
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
17
|
+
logger.addHandler(logging.NullHandler())
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class EmailSender:
|
|
@@ -25,7 +27,7 @@ class EmailSender:
|
|
|
25
27
|
user_email_password: str,
|
|
26
28
|
port: int = 587,
|
|
27
29
|
timeout: int = 10,
|
|
28
|
-
validate_emails: bool = True
|
|
30
|
+
validate_emails: bool = True,
|
|
29
31
|
) -> None:
|
|
30
32
|
self.user_email = self._validate_email(user_email) if validate_emails else user_email
|
|
31
33
|
self.user_email_password = user_email_password
|
|
@@ -33,11 +35,31 @@ class EmailSender:
|
|
|
33
35
|
self.port = port
|
|
34
36
|
self.timeout = timeout
|
|
35
37
|
self.validate_emails = validate_emails
|
|
38
|
+
template_dir = Path(__file__).resolve().parent / "templates"
|
|
36
39
|
self.template_env = Environment(
|
|
37
|
-
loader=FileSystemLoader(
|
|
40
|
+
loader=FileSystemLoader(str(template_dir)),
|
|
38
41
|
autoescape=select_autoescape(['html', 'xml'])
|
|
39
42
|
)
|
|
40
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
|
+
)
|
|
41
63
|
|
|
42
64
|
def _validate_email(self, email_address: str) -> str:
|
|
43
65
|
"""Validate and normalize email address using email-validator."""
|
|
@@ -52,8 +74,7 @@ class EmailSender:
|
|
|
52
74
|
self,
|
|
53
75
|
subject: str,
|
|
54
76
|
recipients: Iterable[str],
|
|
55
|
-
cc: Optional[Iterable[str]] = None
|
|
56
|
-
bcc: Optional[Iterable[str]] = None
|
|
77
|
+
cc: Optional[Iterable[str]] = None
|
|
57
78
|
) -> MIMEMultipart:
|
|
58
79
|
"""Create MIME message with proper headers."""
|
|
59
80
|
msg = MIMEMultipart()
|
|
@@ -70,10 +91,6 @@ class EmailSender:
|
|
|
70
91
|
validated_cc = [self._validate_email(c) for c in cc] if self.validate_emails else cc
|
|
71
92
|
msg['Cc'] = COMMASPACE.join(validated_cc)
|
|
72
93
|
|
|
73
|
-
if bcc:
|
|
74
|
-
validated_bcc = [self._validate_email(b) for b in bcc] if self.validate_emails else bcc
|
|
75
|
-
msg['Bcc'] = COMMASPACE.join(validated_bcc)
|
|
76
|
-
|
|
77
94
|
return msg
|
|
78
95
|
|
|
79
96
|
def _add_attachments(self, msg: MIMEMultipart, attachments: Iterable[str]) -> None:
|
|
@@ -105,7 +122,12 @@ class EmailSender:
|
|
|
105
122
|
html: bool = False
|
|
106
123
|
) -> None:
|
|
107
124
|
"""Synchronous email sending with improved error handling."""
|
|
108
|
-
|
|
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)
|
|
109
131
|
msg.attach(MIMEText(message_body, 'html' if html else 'plain'))
|
|
110
132
|
|
|
111
133
|
if attachments:
|
|
@@ -116,7 +138,12 @@ class EmailSender:
|
|
|
116
138
|
if use_tls:
|
|
117
139
|
server.starttls(context=self.ssl_context)
|
|
118
140
|
server.login(self.user_email, self.user_email_password)
|
|
119
|
-
|
|
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)
|
|
120
147
|
except smtplib.SMTPException as e:
|
|
121
148
|
logger.error(f"SMTP error occurred: {str(e)}")
|
|
122
149
|
raise
|
|
@@ -124,6 +151,57 @@ class EmailSender:
|
|
|
124
151
|
logger.error(f"Unexpected error: {str(e)}")
|
|
125
152
|
raise
|
|
126
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
|
+
|
|
127
205
|
def send_template(
|
|
128
206
|
self,
|
|
129
207
|
recipient: str,
|
|
@@ -135,6 +213,7 @@ class EmailSender:
|
|
|
135
213
|
use_tls: bool = True
|
|
136
214
|
) -> None:
|
|
137
215
|
"""Send email using Jinja2 template with autoescaping."""
|
|
216
|
+
logger.info("Sending templated email to %s using %s", recipient, template_name)
|
|
138
217
|
template = self.template_env.get_template(template_name)
|
|
139
218
|
html_content = template.render(**context)
|
|
140
219
|
self.send([recipient], subject, html_content, cc=cc, attachments=attachments, use_tls=use_tls, html=True)
|
|
@@ -153,8 +232,7 @@ class SendAgent(EmailSender):
|
|
|
153
232
|
cc: Optional[List[str]] = None,
|
|
154
233
|
bcc: Optional[List[str]] = None,
|
|
155
234
|
attachments: Optional[List[str]] = None,
|
|
156
|
-
tls: bool = True
|
|
157
|
-
server_quit: bool = False
|
|
235
|
+
tls: bool = True
|
|
158
236
|
) -> None:
|
|
159
237
|
logger.warning("SendAgent is deprecated, use EmailSender instead")
|
|
160
238
|
|
|
@@ -169,8 +247,6 @@ class SendAgent(EmailSender):
|
|
|
169
247
|
use_tls=tls
|
|
170
248
|
)
|
|
171
249
|
|
|
172
|
-
if server_quit:
|
|
173
|
-
self.server.quit()
|
|
174
250
|
|
|
175
251
|
def send_mail_with_template(
|
|
176
252
|
self,
|
|
@@ -180,8 +256,7 @@ class SendAgent(EmailSender):
|
|
|
180
256
|
template_vars: dict,
|
|
181
257
|
cc: Optional[List[str]] = None,
|
|
182
258
|
attachments: Optional[List[str]] = None,
|
|
183
|
-
tls: bool = True
|
|
184
|
-
server_quit: bool = False
|
|
259
|
+
tls: bool = True
|
|
185
260
|
) -> None:
|
|
186
261
|
logger.warning("send_mail_with_template is deprecated, use send_template instead")
|
|
187
262
|
|
|
@@ -195,24 +270,3 @@ class SendAgent(EmailSender):
|
|
|
195
270
|
use_tls=tls
|
|
196
271
|
)
|
|
197
272
|
|
|
198
|
-
if server_quit:
|
|
199
|
-
self.server.quit()
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# Example usage
|
|
203
|
-
if __name__ == "__main__":
|
|
204
|
-
sender = EmailSender(
|
|
205
|
-
user_email="your@email.com",
|
|
206
|
-
server_smtp_address="smtp.example.com",
|
|
207
|
-
user_email_password="password",
|
|
208
|
-
port=587
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
# Sync send
|
|
212
|
-
sender.send(
|
|
213
|
-
recipients=["gh.rambod@gmail.com"],
|
|
214
|
-
subject="Modern Email",
|
|
215
|
-
message_body="<h1>HTML Content</h1>",
|
|
216
|
-
html=True,
|
|
217
|
-
attachments=["important.pdf"]
|
|
218
|
-
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: MailToolsBox
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A modern and efficient Python library for sending emails with SMTP, Jinja2 templates, and attachments.
|
|
5
5
|
Home-page: https://github.com/rambod/MailToolsBox
|
|
6
6
|
Download-URL: https://github.com/rambod/MailToolsBox/archive/refs/tags/v1.0.0.tar.gz
|
|
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
License-File: LICENSE.txt
|
|
25
25
|
Requires-Dist: Jinja2>=3.0.2
|
|
26
26
|
Requires-Dist: email-validator>=2.0.0
|
|
27
|
+
Requires-Dist: aiosmtplib>=2.0.0
|
|
27
28
|
Dynamic: author
|
|
28
29
|
Dynamic: author-email
|
|
29
30
|
Dynamic: classifier
|
|
@@ -33,6 +34,7 @@ Dynamic: download-url
|
|
|
33
34
|
Dynamic: home-page
|
|
34
35
|
Dynamic: keywords
|
|
35
36
|
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
36
38
|
Dynamic: requires-dist
|
|
37
39
|
Dynamic: requires-python
|
|
38
40
|
Dynamic: summary
|
|
@@ -49,8 +51,11 @@ MailToolsBox is a modern, feature-rich Python package designed for sending and m
|
|
|
49
51
|
- **Attachment handling**
|
|
50
52
|
- **Template-based email rendering using Jinja2**
|
|
51
53
|
- **Secure email transactions with TLS/SSL**
|
|
54
|
+
- **Asynchronous email sending via `send_async`**
|
|
52
55
|
- **Email address validation**
|
|
53
56
|
- **Logging for debugging and monitoring**
|
|
57
|
+
- **Bulk email sending with `send_bulk`**
|
|
58
|
+
- **Convenient configuration via environment variables using `from_env`**
|
|
54
59
|
- **Backward compatibility with `SendAgent`**
|
|
55
60
|
|
|
56
61
|
---
|
|
@@ -157,7 +162,40 @@ sender.send(
|
|
|
157
162
|
|
|
158
163
|
---
|
|
159
164
|
|
|
160
|
-
### 5. **
|
|
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`**
|
|
161
199
|
|
|
162
200
|
For those migrating from earlier versions, `SendAgent` ensures seamless compatibility:
|
|
163
201
|
|
|
@@ -174,10 +212,29 @@ legacy_sender = SendAgent(
|
|
|
174
212
|
legacy_sender.send_mail(
|
|
175
213
|
recipient_email=["recipient@example.com"],
|
|
176
214
|
subject="Legacy Compatibility Test",
|
|
177
|
-
|
|
215
|
+
message_body="Testing backward compatibility."
|
|
178
216
|
)
|
|
179
217
|
```
|
|
180
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
|
+
|
|
181
238
|
---
|
|
182
239
|
|
|
183
240
|
## Configuration & Security Best Practices
|
|
@@ -189,14 +246,7 @@ legacy_sender.send_mail(
|
|
|
189
246
|
Example using environment variables:
|
|
190
247
|
|
|
191
248
|
```python
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
sender = EmailSender(
|
|
195
|
-
user_email=os.getenv("EMAIL"),
|
|
196
|
-
server_smtp_address=os.getenv("SMTP_SERVER"),
|
|
197
|
-
user_email_password=os.getenv("EMAIL_PASSWORD"),
|
|
198
|
-
port=int(os.getenv("SMTP_PORT", 587))
|
|
199
|
-
)
|
|
249
|
+
sender = EmailSender.from_env()
|
|
200
250
|
```
|
|
201
251
|
|
|
202
252
|
---
|
|
@@ -234,7 +284,7 @@ MailToolsBox is an open-source project. Contributions are welcome! To contribute
|
|
|
234
284
|
3. Implement changes and write tests.
|
|
235
285
|
4. Submit a pull request for review.
|
|
236
286
|
|
|
237
|
-
For discussions, visit **[rambod.net](https://www.rambod.net)**.
|
|
287
|
+
For discussions, visit **[rambod.net](https://www.rambod.net)**. **[mailtoolsbox](https://rambod.net/portfolio/mailtoolsbox/)**
|
|
238
288
|
|
|
239
289
|
---
|
|
240
290
|
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: MailToolsBox
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A modern and efficient Python library for sending emails with SMTP, Jinja2 templates, and attachments.
|
|
5
5
|
Home-page: https://github.com/rambod/MailToolsBox
|
|
6
6
|
Download-URL: https://github.com/rambod/MailToolsBox/archive/refs/tags/v1.0.0.tar.gz
|
|
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
License-File: LICENSE.txt
|
|
25
25
|
Requires-Dist: Jinja2>=3.0.2
|
|
26
26
|
Requires-Dist: email-validator>=2.0.0
|
|
27
|
+
Requires-Dist: aiosmtplib>=2.0.0
|
|
27
28
|
Dynamic: author
|
|
28
29
|
Dynamic: author-email
|
|
29
30
|
Dynamic: classifier
|
|
@@ -33,6 +34,7 @@ Dynamic: download-url
|
|
|
33
34
|
Dynamic: home-page
|
|
34
35
|
Dynamic: keywords
|
|
35
36
|
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
36
38
|
Dynamic: requires-dist
|
|
37
39
|
Dynamic: requires-python
|
|
38
40
|
Dynamic: summary
|
|
@@ -49,8 +51,11 @@ MailToolsBox is a modern, feature-rich Python package designed for sending and m
|
|
|
49
51
|
- **Attachment handling**
|
|
50
52
|
- **Template-based email rendering using Jinja2**
|
|
51
53
|
- **Secure email transactions with TLS/SSL**
|
|
54
|
+
- **Asynchronous email sending via `send_async`**
|
|
52
55
|
- **Email address validation**
|
|
53
56
|
- **Logging for debugging and monitoring**
|
|
57
|
+
- **Bulk email sending with `send_bulk`**
|
|
58
|
+
- **Convenient configuration via environment variables using `from_env`**
|
|
54
59
|
- **Backward compatibility with `SendAgent`**
|
|
55
60
|
|
|
56
61
|
---
|
|
@@ -157,7 +162,40 @@ sender.send(
|
|
|
157
162
|
|
|
158
163
|
---
|
|
159
164
|
|
|
160
|
-
### 5. **
|
|
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`**
|
|
161
199
|
|
|
162
200
|
For those migrating from earlier versions, `SendAgent` ensures seamless compatibility:
|
|
163
201
|
|
|
@@ -174,10 +212,29 @@ legacy_sender = SendAgent(
|
|
|
174
212
|
legacy_sender.send_mail(
|
|
175
213
|
recipient_email=["recipient@example.com"],
|
|
176
214
|
subject="Legacy Compatibility Test",
|
|
177
|
-
|
|
215
|
+
message_body="Testing backward compatibility."
|
|
178
216
|
)
|
|
179
217
|
```
|
|
180
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
|
+
|
|
181
238
|
---
|
|
182
239
|
|
|
183
240
|
## Configuration & Security Best Practices
|
|
@@ -189,14 +246,7 @@ legacy_sender.send_mail(
|
|
|
189
246
|
Example using environment variables:
|
|
190
247
|
|
|
191
248
|
```python
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
sender = EmailSender(
|
|
195
|
-
user_email=os.getenv("EMAIL"),
|
|
196
|
-
server_smtp_address=os.getenv("SMTP_SERVER"),
|
|
197
|
-
user_email_password=os.getenv("EMAIL_PASSWORD"),
|
|
198
|
-
port=int(os.getenv("SMTP_PORT", 587))
|
|
199
|
-
)
|
|
249
|
+
sender = EmailSender.from_env()
|
|
200
250
|
```
|
|
201
251
|
|
|
202
252
|
---
|
|
@@ -234,7 +284,7 @@ MailToolsBox is an open-source project. Contributions are welcome! To contribute
|
|
|
234
284
|
3. Implement changes and write tests.
|
|
235
285
|
4. Submit a pull request for review.
|
|
236
286
|
|
|
237
|
-
For discussions, visit **[rambod.net](https://www.rambod.net)**.
|
|
287
|
+
For discussions, visit **[rambod.net](https://www.rambod.net)**. **[mailtoolsbox](https://rambod.net/portfolio/mailtoolsbox/)**
|
|
238
288
|
|
|
239
289
|
---
|
|
240
290
|
|
|
@@ -10,8 +10,11 @@ MailToolsBox is a modern, feature-rich Python package designed for sending and m
|
|
|
10
10
|
- **Attachment handling**
|
|
11
11
|
- **Template-based email rendering using Jinja2**
|
|
12
12
|
- **Secure email transactions with TLS/SSL**
|
|
13
|
+
- **Asynchronous email sending via `send_async`**
|
|
13
14
|
- **Email address validation**
|
|
14
15
|
- **Logging for debugging and monitoring**
|
|
16
|
+
- **Bulk email sending with `send_bulk`**
|
|
17
|
+
- **Convenient configuration via environment variables using `from_env`**
|
|
15
18
|
- **Backward compatibility with `SendAgent`**
|
|
16
19
|
|
|
17
20
|
---
|
|
@@ -118,7 +121,40 @@ sender.send(
|
|
|
118
121
|
|
|
119
122
|
---
|
|
120
123
|
|
|
121
|
-
### 5. **
|
|
124
|
+
### 5. **Asynchronous Email Sending**
|
|
125
|
+
|
|
126
|
+
Send emails without blocking using `send_async` and `asyncio`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import asyncio
|
|
130
|
+
|
|
131
|
+
async def main():
|
|
132
|
+
await sender.send_async(
|
|
133
|
+
recipients=["recipient@example.com"],
|
|
134
|
+
subject="Async Example",
|
|
135
|
+
message_body="This email was sent asynchronously!"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 6. **Bulk Email Sending**
|
|
144
|
+
|
|
145
|
+
Send the same message to many recipients individually, ensuring privacy:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
sender.send_bulk(
|
|
149
|
+
recipients=["user1@example.com", "user2@example.com"],
|
|
150
|
+
subject="Announcement",
|
|
151
|
+
message_body="This email is sent separately to each recipient!",
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### 7. **Backward Compatibility with `SendAgent`**
|
|
122
158
|
|
|
123
159
|
For those migrating from earlier versions, `SendAgent` ensures seamless compatibility:
|
|
124
160
|
|
|
@@ -135,10 +171,29 @@ legacy_sender = SendAgent(
|
|
|
135
171
|
legacy_sender.send_mail(
|
|
136
172
|
recipient_email=["recipient@example.com"],
|
|
137
173
|
subject="Legacy Compatibility Test",
|
|
138
|
-
|
|
174
|
+
message_body="Testing backward compatibility."
|
|
139
175
|
)
|
|
140
176
|
```
|
|
141
177
|
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### 8. **Retrieving Emails via IMAP**
|
|
181
|
+
|
|
182
|
+
`ImapAgent` can be used to fetch messages from an IMAP mailbox. It supports
|
|
183
|
+
loading configuration from environment variables with `from_env` and can be
|
|
184
|
+
used as a context manager to ensure connections are closed properly.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from MailToolsBox import ImapAgent
|
|
188
|
+
|
|
189
|
+
# Using environment variables IMAP_EMAIL, IMAP_PASSWORD and IMAP_SERVER
|
|
190
|
+
agent = ImapAgent.from_env()
|
|
191
|
+
|
|
192
|
+
with agent as imap:
|
|
193
|
+
imap.download_mail_text(path="/tmp/")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
|
|
142
197
|
---
|
|
143
198
|
|
|
144
199
|
## Configuration & Security Best Practices
|
|
@@ -150,14 +205,7 @@ legacy_sender.send_mail(
|
|
|
150
205
|
Example using environment variables:
|
|
151
206
|
|
|
152
207
|
```python
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
sender = EmailSender(
|
|
156
|
-
user_email=os.getenv("EMAIL"),
|
|
157
|
-
server_smtp_address=os.getenv("SMTP_SERVER"),
|
|
158
|
-
user_email_password=os.getenv("EMAIL_PASSWORD"),
|
|
159
|
-
port=int(os.getenv("SMTP_PORT", 587))
|
|
160
|
-
)
|
|
208
|
+
sender = EmailSender.from_env()
|
|
161
209
|
```
|
|
162
210
|
|
|
163
211
|
---
|
|
@@ -195,7 +243,7 @@ MailToolsBox is an open-source project. Contributions are welcome! To contribute
|
|
|
195
243
|
3. Implement changes and write tests.
|
|
196
244
|
4. Submit a pull request for review.
|
|
197
245
|
|
|
198
|
-
For discussions, visit **[rambod.net](https://www.rambod.net)**.
|
|
246
|
+
For discussions, visit **[rambod.net](https://www.rambod.net)**. **[mailtoolsbox](https://rambod.net/portfolio/mailtoolsbox/)**
|
|
199
247
|
|
|
200
248
|
---
|
|
201
249
|
|
|
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|
|
3
3
|
setup(
|
|
4
4
|
name='MailToolsBox',
|
|
5
5
|
packages=find_packages(),
|
|
6
|
-
version='1.0.
|
|
6
|
+
version='1.0.1', # Increased version for major revamp
|
|
7
7
|
license='MIT',
|
|
8
8
|
description='A modern and efficient Python library for sending emails with SMTP, Jinja2 templates, and attachments.',
|
|
9
9
|
long_description=open('README.md', encoding='utf-8').read(),
|
|
@@ -15,7 +15,8 @@ setup(
|
|
|
15
15
|
keywords=['Mail', 'SMTP', 'email', 'tools', 'attachments', 'Jinja2', 'Python', 'email-validation'],
|
|
16
16
|
install_requires=[
|
|
17
17
|
"Jinja2>=3.0.2",
|
|
18
|
-
"email-validator>=2.0.0"
|
|
18
|
+
"email-validator>=2.0.0",
|
|
19
|
+
"aiosmtplib>=2.0.0"
|
|
19
20
|
],
|
|
20
21
|
classifiers=[
|
|
21
22
|
'Development Status :: 5 - Production/Stable',
|
|
File without changes
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import email
|
|
3
|
+
from email.mime.text import MIMEText
|
|
4
|
+
from unittest import mock
|
|
5
|
+
import os
|
|
6
|
+
import types
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
# Provide dummy aiosmtplib for indirect imports via mailSender
|
|
12
|
+
sys.modules.setdefault(
|
|
13
|
+
"aiosmtplib",
|
|
14
|
+
types.SimpleNamespace(SMTP=None, errors=types.SimpleNamespace(SMTPException=Exception)),
|
|
15
|
+
)
|
|
16
|
+
# Minimal stub for jinja2 used by EmailSender imports
|
|
17
|
+
sys.modules.setdefault(
|
|
18
|
+
"jinja2",
|
|
19
|
+
types.SimpleNamespace(
|
|
20
|
+
Environment=lambda **kwargs: types.SimpleNamespace(get_template=lambda name: types.SimpleNamespace(render=lambda **kw: "")),
|
|
21
|
+
FileSystemLoader=lambda *args, **kwargs: None,
|
|
22
|
+
select_autoescape=lambda x: None,
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
sys.modules.setdefault(
|
|
26
|
+
"email_validator",
|
|
27
|
+
types.SimpleNamespace(
|
|
28
|
+
validate_email=lambda email, check_deliverability=False: types.SimpleNamespace(normalized=email),
|
|
29
|
+
EmailNotValidError=Exception,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from MailToolsBox.imapClient import ImapAgent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DummyMail:
|
|
37
|
+
def __init__(self, message_bytes):
|
|
38
|
+
self.message_bytes = message_bytes
|
|
39
|
+
self.closed = False
|
|
40
|
+
self.logged_in = False
|
|
41
|
+
self.selected = None
|
|
42
|
+
def login(self, user, password):
|
|
43
|
+
self.logged_in = True
|
|
44
|
+
def select(self, mailbox):
|
|
45
|
+
self.selected = mailbox
|
|
46
|
+
return ('OK', [b''])
|
|
47
|
+
def uid(self, command, *args):
|
|
48
|
+
if command == 'search':
|
|
49
|
+
return ('OK', [b'1'])
|
|
50
|
+
elif command == 'fetch':
|
|
51
|
+
return ('OK', [(None, self.message_bytes)])
|
|
52
|
+
def close(self):
|
|
53
|
+
self.closed = True
|
|
54
|
+
def logout(self):
|
|
55
|
+
self.closed = True
|
|
56
|
+
def __enter__(self):
|
|
57
|
+
return self
|
|
58
|
+
def __exit__(self, exc_type, exc, tb):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def sample_message_bytes():
|
|
63
|
+
msg = MIMEText('body text')
|
|
64
|
+
msg['From'] = 'from@example.com'
|
|
65
|
+
msg['To'] = 'to@example.com'
|
|
66
|
+
msg['Subject'] = 'Test'
|
|
67
|
+
msg['Date'] = 'Wed, 20 Sep 2023 12:00:00 -0000'
|
|
68
|
+
return msg.as_bytes()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_login_account(monkeypatch):
|
|
72
|
+
dummy = DummyMail(sample_message_bytes())
|
|
73
|
+
monkeypatch.setattr('imaplib.IMAP4_SSL', lambda addr: dummy)
|
|
74
|
+
agent = ImapAgent('user', 'pass', 'imap.example.com')
|
|
75
|
+
agent.login_account()
|
|
76
|
+
assert agent.mail is dummy
|
|
77
|
+
assert dummy.logged_in
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_download_mail_text(tmp_path, monkeypatch):
|
|
81
|
+
message_bytes = sample_message_bytes()
|
|
82
|
+
dummy = DummyMail(message_bytes)
|
|
83
|
+
agent = ImapAgent('user', 'pass', 'imap.example.com')
|
|
84
|
+
agent.mail = dummy
|
|
85
|
+
|
|
86
|
+
agent.download_mail_text(path=str(tmp_path) + os.sep)
|
|
87
|
+
|
|
88
|
+
file_path = tmp_path / 'email.txt'
|
|
89
|
+
assert file_path.exists()
|
|
90
|
+
content = file_path.read_text()
|
|
91
|
+
assert 'body text' in content
|
|
92
|
+
assert dummy.closed
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_download_mail_json(tmp_path, monkeypatch):
|
|
96
|
+
message_bytes = sample_message_bytes()
|
|
97
|
+
dummy = DummyMail(message_bytes)
|
|
98
|
+
monkeypatch.setattr(ImapAgent, 'login_account', lambda self: None)
|
|
99
|
+
monkeypatch.setattr('imaplib.IMAP4_SSL', lambda addr: dummy)
|
|
100
|
+
|
|
101
|
+
agent = ImapAgent('user', 'pass', 'imap.example.com')
|
|
102
|
+
result = agent.download_mail_json(save=True, path=str(tmp_path) + os.sep)
|
|
103
|
+
|
|
104
|
+
data = json.loads(result)
|
|
105
|
+
assert isinstance(data, list)
|
|
106
|
+
assert data[0]['subject'] == 'Test'
|
|
107
|
+
file_path = tmp_path / 'mail.json'
|
|
108
|
+
assert file_path.exists()
|
|
109
|
+
assert dummy.closed
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_download_mail_msg(tmp_path):
|
|
113
|
+
message_bytes = sample_message_bytes()
|
|
114
|
+
dummy = DummyMail(message_bytes)
|
|
115
|
+
agent = ImapAgent('user', 'pass', 'imap.example.com')
|
|
116
|
+
agent.login_account = lambda: setattr(agent, 'mail', dummy)
|
|
117
|
+
|
|
118
|
+
agent.download_mail_msg(path=str(tmp_path) + os.sep)
|
|
119
|
+
|
|
120
|
+
file_path = tmp_path / 'email_0.msg'
|
|
121
|
+
assert file_path.exists()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_from_env(monkeypatch):
|
|
125
|
+
monkeypatch.setenv("IMAP_EMAIL", "env@example.com")
|
|
126
|
+
monkeypatch.setenv("IMAP_PASSWORD", "secret")
|
|
127
|
+
monkeypatch.setenv("IMAP_SERVER", "imap.env.com")
|
|
128
|
+
|
|
129
|
+
agent = ImapAgent.from_env()
|
|
130
|
+
|
|
131
|
+
assert agent.email_account == "env@example.com"
|
|
132
|
+
assert agent.password == "secret"
|
|
133
|
+
assert agent.server_address == "imap.env.com"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_context_manager(monkeypatch):
|
|
137
|
+
dummy = DummyMail(sample_message_bytes())
|
|
138
|
+
monkeypatch.setattr('imaplib.IMAP4_SSL', lambda addr: dummy)
|
|
139
|
+
|
|
140
|
+
with ImapAgent('user', 'pass', 'imap.example.com') as agent:
|
|
141
|
+
assert agent.mail is dummy
|
|
142
|
+
assert dummy.logged_in
|
|
143
|
+
assert dummy.closed
|
|
144
|
+
assert agent.mail is None
|
|
145
|
+
|
|
146
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import smtplib
|
|
3
|
+
from unittest import mock
|
|
4
|
+
import types
|
|
5
|
+
import sys
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
# Provide dummy aiosmtplib to satisfy imports
|
|
9
|
+
sys.modules.setdefault(
|
|
10
|
+
"aiosmtplib",
|
|
11
|
+
types.SimpleNamespace(SMTP=None, errors=types.SimpleNamespace(SMTPException=Exception)),
|
|
12
|
+
)
|
|
13
|
+
# Minimal stub for jinja2
|
|
14
|
+
sys.modules.setdefault(
|
|
15
|
+
"jinja2",
|
|
16
|
+
types.SimpleNamespace(
|
|
17
|
+
Environment=lambda **kwargs: types.SimpleNamespace(get_template=lambda name: types.SimpleNamespace(render=lambda **kw: "")),
|
|
18
|
+
FileSystemLoader=lambda *args, **kwargs: None,
|
|
19
|
+
select_autoescape=lambda x: None,
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
sys.modules.setdefault(
|
|
23
|
+
"email_validator",
|
|
24
|
+
types.SimpleNamespace(
|
|
25
|
+
validate_email=lambda email, check_deliverability=False: types.SimpleNamespace(normalized=email),
|
|
26
|
+
EmailNotValidError=Exception,
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from MailToolsBox.mailSender import EmailSender
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_email_sender_send(monkeypatch):
|
|
34
|
+
smtp_instance = mock.MagicMock()
|
|
35
|
+
smtp_instance.__enter__.return_value = smtp_instance
|
|
36
|
+
smtp_instance.__exit__.return_value = None
|
|
37
|
+
|
|
38
|
+
smtp_class = mock.MagicMock(return_value=smtp_instance)
|
|
39
|
+
monkeypatch.setattr(smtplib, "SMTP", smtp_class)
|
|
40
|
+
|
|
41
|
+
sender = EmailSender(
|
|
42
|
+
user_email="user@example.com",
|
|
43
|
+
server_smtp_address="smtp.example.com",
|
|
44
|
+
user_email_password="pass",
|
|
45
|
+
port=25,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
sender.send(
|
|
49
|
+
recipients=["to@example.com"],
|
|
50
|
+
subject="Subj",
|
|
51
|
+
message_body="Body",
|
|
52
|
+
cc=["cc@example.com"],
|
|
53
|
+
bcc=["bcc@example.com"],
|
|
54
|
+
use_tls=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
smtp_class.assert_called_with("smtp.example.com", 25, timeout=10)
|
|
58
|
+
smtp_instance.starttls.assert_called()
|
|
59
|
+
smtp_instance.login.assert_called_with("user@example.com", "pass")
|
|
60
|
+
smtp_instance.send_message.assert_called()
|
|
61
|
+
|
|
62
|
+
args, kwargs = smtp_instance.send_message.call_args
|
|
63
|
+
msg = args[0]
|
|
64
|
+
to_addrs = kwargs["to_addrs"]
|
|
65
|
+
assert set(to_addrs) == {"to@example.com", "cc@example.com", "bcc@example.com"}
|
|
66
|
+
assert msg["To"] == "to@example.com"
|
|
67
|
+
assert msg["Cc"] == "cc@example.com"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_email_sender_from_env(monkeypatch):
|
|
71
|
+
monkeypatch.setenv("EMAIL", "env@example.com")
|
|
72
|
+
monkeypatch.setenv("SMTP_SERVER", "smtp.env.com")
|
|
73
|
+
monkeypatch.setenv("EMAIL_PASSWORD", "secret")
|
|
74
|
+
monkeypatch.setenv("SMTP_PORT", "2525")
|
|
75
|
+
|
|
76
|
+
sender = EmailSender.from_env()
|
|
77
|
+
|
|
78
|
+
assert sender.user_email == "env@example.com"
|
|
79
|
+
assert sender.server_smtp_address == "smtp.env.com"
|
|
80
|
+
assert sender.user_email_password == "secret"
|
|
81
|
+
assert sender.port == 2525
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_send_bulk(monkeypatch):
|
|
85
|
+
sent = []
|
|
86
|
+
|
|
87
|
+
class DummySender(EmailSender):
|
|
88
|
+
def __init__(self):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
def send(self, recipients, subject, message_body, **kwargs):
|
|
92
|
+
sent.append(recipients[0])
|
|
93
|
+
|
|
94
|
+
sender = DummySender()
|
|
95
|
+
|
|
96
|
+
sender.send_bulk(["a@example.com", "b@example.com"], "subj", "body")
|
|
97
|
+
|
|
98
|
+
assert sent == ["a@example.com", "b@example.com"]
|
|
99
|
+
|
|
100
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|