Skip to content

Commit b82980a

Browse files
authored
Merge pull request #41 from itk-dev-rpa/release/2.1.0
Release/2.1.0
2 parents b9fb89e + 6488106 commit b82980a

File tree

7 files changed

+193
-2
lines changed

7 files changed

+193
-2
lines changed

changelog.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.1.0] - 2024-04-09
11+
12+
### Added
13+
14+
- smtp_util: For sending emails using the smtp protocol.
15+
- Tests for smtp_util.
16+
1017
## [2.0.0] - 2024-04-03
1118

1219
### Changed
@@ -81,7 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8188

8289
- Initial release
8390

84-
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.0.0...HEAD
91+
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.1.0...HEAD
92+
[2.1.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.0
8593
[2.0.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.0.0
8694
[1.3.1] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/1.3.1
8795
[1.3.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/1.3.0

itk_dev_shared_components/smtp/__init__.py

Whitespace-only changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""This module contains a single function for sending emails using the SMTP protocol."""
2+
3+
from typing import Sequence
4+
from email.message import EmailMessage
5+
import smtplib
6+
from io import BytesIO
7+
from dataclasses import dataclass
8+
import mimetypes
9+
10+
11+
@dataclass
12+
class EmailAttachment:
13+
"""A simple dataclass representing an email attachment."""
14+
file: BytesIO
15+
file_name: str
16+
17+
18+
def send_email(receiver: str | list[str], sender: str, subject: str, body: str, smtp_server: str, smtp_port: int,
19+
html_body: bool = False, attachments: Sequence[EmailAttachment] | None = None) -> None:
20+
"""Send an email using the SMTP protocol.
21+
22+
Args:
23+
receiver: The email or list of emails to send the message to.
24+
sender: The sender email of the message.
25+
subject: The message subject.
26+
body: The message body.
27+
smtp_server: The name of the smtp server.
28+
smtp_port: The port of the smtp server.
29+
html_body: Wether the body is html or just plain text. Defaults to False.
30+
attachments: A list of Attachment objects. Defaults to None.
31+
"""
32+
msg = EmailMessage()
33+
msg['to'] = receiver
34+
msg['from'] = sender
35+
msg['subject'] = subject
36+
37+
# Set body
38+
if html_body:
39+
msg.set_content("Please enable HTML to view this message.")
40+
msg.add_alternative(body, subtype='html')
41+
else:
42+
msg.set_content(body)
43+
44+
# Attach files
45+
if attachments:
46+
for attachment in attachments:
47+
mime = mimetypes.guess_type(attachment.file_name)[0]
48+
main, sub = mime.split("/") if mime else ("application", "octet-stream")
49+
attachment.file.seek(0)
50+
msg.add_attachment(attachment.file.read(), maintype=main, subtype=sub, filename=attachment.file_name)
51+
52+
# Send message
53+
with smtplib.SMTP(smtp_server, smtp_port) as smtp:
54+
smtp.starttls()
55+
smtp.send_message(msg)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "itk_dev_shared_components"
7-
version = "2.0.0"
7+
version = "2.1.0"
88
authors = [
99
{ name="ITK Development", email="itk-rpa@mkb.aarhus.dk" },
1010
]

tests/readme.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Test readme
2+
3+
## Running the tests
4+
5+
To run all tests you can use the `run_tests.bat` file in the tests folder.
6+
This bat file sets up a new virtual environment and installs the package before running all tests.
7+
8+
Alternatively you can run each test file separately by simply running them as Python scripts.
9+
10+
## SMTP
11+
12+
For testing SMTP you need [Mailpit](https://mailpit.axllent.org/) running on localhost.
13+
14+
Since smtp_util uses STARTTLS you need to configure Mailpit for this. [More info here.](https://mailpit.axllent.org/docs/configuration/smtp/#smtp-with-starttls)
15+
16+
By default the tests use the following cofigurations for Mailpit:
17+
18+
- Host: localhost
19+
- smtp port: 1025
20+
- http port: 8025
21+
22+
These can be changed by using the environment variables:
23+
24+
- mailpit_host
25+
- mailpit_smtp_port
26+
- mailpit_http_port
27+
28+
Use this command to generate the TLS certificates:
29+
30+
```bash
31+
openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -sha256
32+
```
33+
34+
And this command to launch Mailpit:
35+
36+
```bash
37+
mailpit --smtp-tls-cert cert.pem --smtp-tls-key key.pem
38+
```

tests/test_smtp/__init__.py

Whitespace-only changes.

tests/test_smtp/test_smtp_util.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Tests relating to the module smtp.smtp_util."""
2+
3+
import unittest
4+
from io import BytesIO
5+
import os
6+
7+
import requests
8+
9+
from itk_dev_shared_components.smtp import smtp_util
10+
from itk_dev_shared_components.smtp.smtp_util import EmailAttachment
11+
12+
13+
class TestTreeUtil(unittest.TestCase):
14+
"""Tests relating to the module smtp.smtp_util."""
15+
smtp_server = os.getenv("mailpit_host", "localhost")
16+
smtp_port = os.getenv("mailpit_smtp_port", "1025")
17+
http_port = os.getenv("mailpit_http_port", "8025")
18+
19+
def setUp(self) -> None:
20+
url = url = f"http://localhost:{self.http_port}/api/v1/messages"
21+
response = requests.delete(url, timeout=2)
22+
response.raise_for_status()
23+
24+
def test_send_simple(self):
25+
"""Test sending a simple email."""
26+
smtp_util.send_email("test@receiver.com", "idsc@test.dk", "Test simple", "Test body", smtp_server=self.smtp_server, smtp_port=self.smtp_port)
27+
28+
message = self.get_message("Test simple")
29+
self.assertEqual(message["From"]["Address"], "idsc@test.dk")
30+
self.assertEqual(message["To"][0]["Address"], "test@receiver.com")
31+
self.assertEqual(message["Snippet"], "Test body")
32+
33+
def test_send_html(self):
34+
"""Test sending an html email."""
35+
html = "<html><head><style> table {font-family: arial, sans-serif; border-collapse: collapse; width: 100%;} td, th { border: 1px solid #dddddd; text-align: left; padding: 8px;}</style></head><body><h2>HTML Table</h2><table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table></body></html>"
36+
smtp_util.send_email("test@receiver.com", "idsc@test.dk", "Test html", html, html_body=True, smtp_server=self.smtp_server, smtp_port=self.smtp_port)
37+
38+
message = self.get_message("Test html")
39+
self.assertTrue(message["Snippet"].startswith("HTML Table"))
40+
41+
def test_send_attachments(self):
42+
"""Test sending an email with multiple attachments."""
43+
attachments = []
44+
45+
# Generate some attachment files
46+
for i in range(3):
47+
file = BytesIO(b"Hello"*(i+1))
48+
file_name = f"file{i}.txt"
49+
50+
attachments.append(
51+
EmailAttachment(file, file_name)
52+
)
53+
54+
smtp_util.send_email("test@receiver.com", "idsc@test.dk", "Test files", "This email has three attached txt files.", attachments=attachments, smtp_server=self.smtp_server, smtp_port=self.smtp_port)
55+
56+
message = self.get_message("Test files")
57+
self.assertEqual(message["Attachments"], 3)
58+
59+
def test_send_multiple(self):
60+
"""Test sending to multiple receivers."""
61+
smtp_util.send_email(["test@receiver.com", "test@receiver.com"], "idsc@test.dk", "Test multiple", "This email has multiple receivers.", smtp_server=self.smtp_server, smtp_port=self.smtp_port)
62+
63+
message = self.get_message("Test multiple")
64+
self.assertEqual(len(message["To"]), 2)
65+
66+
def get_message(self, subject: str):
67+
"""Get a message from the Mailpit api with the given subject.
68+
https://mailpit.axllent.org/docs/api-v1/view.html#tag--messages
69+
70+
Args:
71+
subject: The email subject to search for.
72+
73+
Returns:
74+
A dict representing the message from Mailpit.
75+
"""
76+
url = f"http://localhost:{self.http_port}/api/v1/messages"
77+
response = requests.get(url, timeout=2).json()
78+
79+
message = None
80+
for msg in response["messages"]:
81+
if msg["Subject"] == subject:
82+
message = msg
83+
break
84+
85+
self.assertIsNotNone(message)
86+
return message
87+
88+
89+
if __name__ == '__main__':
90+
unittest.main()

0 commit comments

Comments
 (0)