Add Mailpit SMTP test server for Notification Service email testing
Adds a fourth Docker service (Mailpit) to capture outgoing emails without delivery, with CLI tool for sending test emails, listing/reading captured messages, and clearing the inbox. Supports BCC pattern matching ScadaLink's notification delivery model.
This commit is contained in:
@@ -8,13 +8,14 @@ Local Docker-based test services for ScadaLink development.
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This starts three services:
|
This starts four services:
|
||||||
|
|
||||||
| Service | Port | Purpose |
|
| Service | Port | Purpose |
|
||||||
|---------|------|---------|
|
|---------|------|---------|
|
||||||
| OPC UA (Azure IoT OPC PLC) | 50000 (OPC UA), 8080 (web) | Simulated OPC UA server with ScadaLink-style tags |
|
| OPC UA (Azure IoT OPC PLC) | 50000 (OPC UA), 8080 (web) | Simulated OPC UA server with ScadaLink-style tags |
|
||||||
| LDAP (GLAuth) | 3893 | Lightweight LDAP with test users/groups matching ScadaLink roles |
|
| LDAP (GLAuth) | 3893 | Lightweight LDAP with test users/groups matching ScadaLink roles |
|
||||||
| MS SQL 2022 | 1433 | Configuration and machine data databases |
|
| MS SQL 2022 | 1433 | Configuration and machine data databases |
|
||||||
|
| SMTP (Mailpit) | 1025 (SMTP), 8025 (web) | Email capture for notification testing |
|
||||||
|
|
||||||
## First-Time SQL Setup
|
## First-Time SQL Setup
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ docker compose down
|
|||||||
|
|
||||||
**Stop a single service** (leave the others running):
|
**Stop a single service** (leave the others running):
|
||||||
```bash
|
```bash
|
||||||
docker compose stop opcua # or: ldap, mssql
|
docker compose stop opcua # or: ldap, mssql, smtp
|
||||||
docker compose start opcua # bring it back without recreating
|
docker compose start opcua # bring it back without recreating
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,11 +71,12 @@ pip install -r tools/requirements.txt
|
|||||||
|
|
||||||
> The `.venv` directory is gitignored.
|
> The `.venv` directory is gitignored.
|
||||||
|
|
||||||
**Quick readiness check** (all three services, with venv active):
|
**Quick readiness check** (all four services, with venv active):
|
||||||
```bash
|
```bash
|
||||||
python tools/opcua_tool.py check
|
python tools/opcua_tool.py check
|
||||||
python tools/ldap_tool.py check
|
python tools/ldap_tool.py check
|
||||||
python tools/mssql_tool.py check
|
python tools/mssql_tool.py check
|
||||||
|
python tools/smtp_tool.py check
|
||||||
```
|
```
|
||||||
|
|
||||||
| Tool | Service | Key Commands |
|
| Tool | Service | Key Commands |
|
||||||
@@ -82,6 +84,7 @@ python tools/mssql_tool.py check
|
|||||||
| `tools/opcua_tool.py` | OPC UA | `check`, `browse`, `read`, `write`, `monitor` |
|
| `tools/opcua_tool.py` | OPC UA | `check`, `browse`, `read`, `write`, `monitor` |
|
||||||
| `tools/ldap_tool.py` | LDAP | `check`, `bind`, `search`, `users`, `groups` |
|
| `tools/ldap_tool.py` | LDAP | `check`, `bind`, `search`, `users`, `groups` |
|
||||||
| `tools/mssql_tool.py` | MS SQL | `check`, `setup`, `query`, `tables` |
|
| `tools/mssql_tool.py` | MS SQL | `check`, `setup`, `query`, `tables` |
|
||||||
|
| `tools/smtp_tool.py` | SMTP (Mailpit) | `check`, `send`, `list`, `read`, `clear` |
|
||||||
|
|
||||||
Each tool supports `--help` for full usage. See the per-service docs below for detailed examples.
|
Each tool supports `--help` for full usage. See the per-service docs below for detailed examples.
|
||||||
|
|
||||||
@@ -93,3 +96,4 @@ See the project root for per-service setup guides:
|
|||||||
- [test_infra_opcua.md](../test_infra_opcua.md) — OPC UA server details
|
- [test_infra_opcua.md](../test_infra_opcua.md) — OPC UA server details
|
||||||
- [test_infra_ldap.md](../test_infra_ldap.md) — LDAP server details
|
- [test_infra_ldap.md](../test_infra_ldap.md) — LDAP server details
|
||||||
- [test_infra_db.md](../test_infra_db.md) — MS SQL database details
|
- [test_infra_db.md](../test_infra_db.md) — MS SQL database details
|
||||||
|
- [test_infra_smtp.md](../test_infra_smtp.md) — SMTP server details (Mailpit)
|
||||||
|
|||||||
@@ -41,5 +41,17 @@ services:
|
|||||||
- ./mssql/setup.sql:/docker-entrypoint-initdb.d/setup.sql:ro
|
- ./mssql/setup.sql:/docker-entrypoint-initdb.d/setup.sql:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
smtp:
|
||||||
|
image: axllent/mailpit:latest
|
||||||
|
container_name: scadalink-smtp
|
||||||
|
ports:
|
||||||
|
- "1025:1025"
|
||||||
|
- "8025:8025"
|
||||||
|
environment:
|
||||||
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||||
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||||
|
MP_MAX_MESSAGES: 500
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
scadalink-mssql-data:
|
scadalink-mssql-data:
|
||||||
|
|||||||
189
infra/tools/smtp_tool.py
Normal file
189
infra/tools/smtp_tool.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""SMTP/Mailpit client tool for ScadaLink test infrastructure."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import email.mime.text
|
||||||
|
import json
|
||||||
|
import smtplib
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SMTP_HOST = "localhost"
|
||||||
|
DEFAULT_SMTP_PORT = 1025
|
||||||
|
DEFAULT_API_URL = "http://localhost:8025/api"
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_check(args):
|
||||||
|
"""Test SMTP connectivity and report Mailpit status."""
|
||||||
|
# Test SMTP connection
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP(args.host, args.port, timeout=5)
|
||||||
|
server.ehlo()
|
||||||
|
smtp_ok = True
|
||||||
|
server.quit()
|
||||||
|
except Exception as e:
|
||||||
|
smtp_ok = False
|
||||||
|
smtp_err = str(e)
|
||||||
|
|
||||||
|
# Test API/web UI
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(f"{args.api}/v1/info")
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
info = json.loads(resp.read())
|
||||||
|
api_ok = True
|
||||||
|
except Exception as e:
|
||||||
|
api_ok = False
|
||||||
|
api_err = str(e)
|
||||||
|
|
||||||
|
print(f"SMTP ({args.host}:{args.port}): {'OK' if smtp_ok else 'FAILED - ' + smtp_err}")
|
||||||
|
print(f"Web UI/API ({args.api}): {'OK' if api_ok else 'FAILED - ' + api_err}")
|
||||||
|
|
||||||
|
if api_ok:
|
||||||
|
print(f"\nMailpit version: {info.get('Version', 'unknown')}")
|
||||||
|
print(f"Database path: {info.get('DatabasePath', 'unknown')}")
|
||||||
|
messages = info.get('Messages', 0)
|
||||||
|
print(f"Stored messages: {messages}")
|
||||||
|
|
||||||
|
if smtp_ok and api_ok:
|
||||||
|
print("\nSMTP server is healthy.")
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_send(args):
|
||||||
|
"""Send a test email via SMTP."""
|
||||||
|
msg = email.mime.text.MIMEText(args.body, "plain")
|
||||||
|
msg["Subject"] = args.subject
|
||||||
|
msg["From"] = args.sender
|
||||||
|
msg["To"] = args.to
|
||||||
|
|
||||||
|
if args.bcc:
|
||||||
|
recipients = [args.to] + [b.strip() for b in args.bcc.split(",")]
|
||||||
|
else:
|
||||||
|
recipients = [args.to]
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP(args.host, args.port, timeout=5)
|
||||||
|
server.ehlo()
|
||||||
|
server.sendmail(args.sender, recipients, msg.as_string())
|
||||||
|
server.quit()
|
||||||
|
bcc_note = f" (BCC: {args.bcc})" if args.bcc else ""
|
||||||
|
print(f"Sent: {args.sender} -> {args.to}{bcc_note}")
|
||||||
|
print(f"Subject: {args.subject}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list(args):
|
||||||
|
"""List messages in Mailpit inbox."""
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(f"{args.api}/v1/messages?limit={args.limit}")
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
total = data.get("messages_count", 0)
|
||||||
|
messages = data.get("messages", [])
|
||||||
|
|
||||||
|
print(f"Messages: {len(messages)} shown / {total} total\n")
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
print("(inbox empty)")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"{'ID':<24} {'Date':<22} {'From':<30} {'To':<30} {'Subject'}")
|
||||||
|
print("-" * 130)
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
msg_id = msg["ID"]
|
||||||
|
date = msg.get("Date", "")[:21]
|
||||||
|
from_addr = msg.get("From", {}).get("Address", "")[:28]
|
||||||
|
to_list = msg.get("To", [])
|
||||||
|
to_addr = (to_list[0].get("Address", "") if to_list else "")[:28]
|
||||||
|
subject = msg.get("Subject", "")[:40]
|
||||||
|
print(f"{msg_id:<24} {date:<22} {from_addr:<30} {to_addr:<30} {subject}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_read(args):
|
||||||
|
"""Read a specific message by ID."""
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(f"{args.api}/v1/message/{args.id}")
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
msg = json.loads(resp.read())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"ID: {msg['ID']}")
|
||||||
|
print(f"Date: {msg.get('Date', '')}")
|
||||||
|
from_info = msg.get("From", {})
|
||||||
|
print(f"From: {from_info.get('Name', '')} <{from_info.get('Address', '')}>")
|
||||||
|
|
||||||
|
for field in ["To", "Cc", "Bcc"]:
|
||||||
|
addrs = msg.get(field, [])
|
||||||
|
if addrs:
|
||||||
|
formatted = ", ".join(f"{a.get('Name', '')} <{a.get('Address', '')}>" for a in addrs)
|
||||||
|
print(f"{field}: {formatted}")
|
||||||
|
|
||||||
|
print(f"Subject: {msg.get('Subject', '')}")
|
||||||
|
print()
|
||||||
|
print(msg.get("Text", "(no text body)"))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_clear(args):
|
||||||
|
"""Delete all messages in Mailpit."""
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(f"{args.api}/v1/messages", method="DELETE")
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
resp.read()
|
||||||
|
print("All messages deleted.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="SMTP/Mailpit client tool for ScadaLink test infrastructure")
|
||||||
|
parser.add_argument("--host", default=DEFAULT_SMTP_HOST, help=f"SMTP host (default: {DEFAULT_SMTP_HOST})")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_SMTP_PORT, help=f"SMTP port (default: {DEFAULT_SMTP_PORT})")
|
||||||
|
parser.add_argument("--api", default=DEFAULT_API_URL, help=f"Mailpit API URL (default: {DEFAULT_API_URL})")
|
||||||
|
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
sub.add_parser("check", help="Test SMTP connectivity and Mailpit status")
|
||||||
|
|
||||||
|
send_p = sub.add_parser("send", help="Send a test email")
|
||||||
|
send_p.add_argument("--sender", default="scada-notifications@company.com",
|
||||||
|
help="From address (default: scada-notifications@company.com)")
|
||||||
|
send_p.add_argument("--to", required=True, help="Recipient address")
|
||||||
|
send_p.add_argument("--bcc", help="Comma-separated BCC addresses")
|
||||||
|
send_p.add_argument("--subject", default="Test notification", help="Subject line")
|
||||||
|
send_p.add_argument("--body", default="This is a test notification from ScadaLink.", help="Message body")
|
||||||
|
|
||||||
|
list_p = sub.add_parser("list", help="List messages in Mailpit inbox")
|
||||||
|
list_p.add_argument("--limit", type=int, default=20, help="Number of messages to show (default: 20)")
|
||||||
|
|
||||||
|
read_p = sub.add_parser("read", help="Read a specific message by ID")
|
||||||
|
read_p.add_argument("--id", required=True, help="Message ID (from list command)")
|
||||||
|
|
||||||
|
sub.add_parser("clear", help="Delete all messages")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"check": cmd_check,
|
||||||
|
"send": cmd_send,
|
||||||
|
"list": cmd_list,
|
||||||
|
"read": cmd_read,
|
||||||
|
"clear": cmd_clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
commands[args.command](args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Test Infrastructure
|
# Test Infrastructure
|
||||||
|
|
||||||
This document describes the local Docker-based test infrastructure for ScadaLink development. Three services provide the external dependencies needed to run and test the system locally.
|
This document describes the local Docker-based test infrastructure for ScadaLink development. Four services provide the external dependencies needed to run and test the system locally.
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ This document describes the local Docker-based test infrastructure for ScadaLink
|
|||||||
| OPC UA Server | `mcr.microsoft.com/iotedge/opc-plc:latest` | 50000 (OPC UA), 8080 (web) | `infra/opcua/nodes.json` |
|
| OPC UA Server | `mcr.microsoft.com/iotedge/opc-plc:latest` | 50000 (OPC UA), 8080 (web) | `infra/opcua/nodes.json` |
|
||||||
| LDAP Server | `glauth/glauth:latest` | 3893 | `infra/glauth/config.toml` |
|
| LDAP Server | `glauth/glauth:latest` | 3893 | `infra/glauth/config.toml` |
|
||||||
| MS SQL 2022 | `mcr.microsoft.com/mssql/server:2022-latest` | 1433 | `infra/mssql/setup.sql` |
|
| MS SQL 2022 | `mcr.microsoft.com/mssql/server:2022-latest` | 1433 | `infra/mssql/setup.sql` |
|
||||||
|
| SMTP (Mailpit) | `axllent/mailpit:latest` | 1025 (SMTP), 8025 (web) | Environment vars |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ Each service has a dedicated document with configuration details, verification s
|
|||||||
- [test_infra_opcua.md](test_infra_opcua.md) — OPC UA test server (Azure IoT OPC PLC)
|
- [test_infra_opcua.md](test_infra_opcua.md) — OPC UA test server (Azure IoT OPC PLC)
|
||||||
- [test_infra_ldap.md](test_infra_ldap.md) — LDAP test server (GLAuth)
|
- [test_infra_ldap.md](test_infra_ldap.md) — LDAP test server (GLAuth)
|
||||||
- [test_infra_db.md](test_infra_db.md) — MS SQL 2022 database
|
- [test_infra_db.md](test_infra_db.md) — MS SQL 2022 database
|
||||||
|
- [test_infra_smtp.md](test_infra_smtp.md) — SMTP test server (Mailpit)
|
||||||
|
|
||||||
## Connection Strings
|
## Connection Strings
|
||||||
|
|
||||||
@@ -51,6 +53,13 @@ For use in `appsettings.Development.json`:
|
|||||||
},
|
},
|
||||||
"OpcUa": {
|
"OpcUa": {
|
||||||
"EndpointUrl": "opc.tcp://localhost:50000"
|
"EndpointUrl": "opc.tcp://localhost:50000"
|
||||||
|
},
|
||||||
|
"Smtp": {
|
||||||
|
"Server": "localhost",
|
||||||
|
"Port": 1025,
|
||||||
|
"AuthMode": "None",
|
||||||
|
"FromAddress": "scada-notifications@company.com",
|
||||||
|
"ConnectionTimeout": 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -60,7 +69,7 @@ For use in `appsettings.Development.json`:
|
|||||||
```bash
|
```bash
|
||||||
cd infra
|
cd infra
|
||||||
docker compose down # stop containers, preserve SQL data volume
|
docker compose down # stop containers, preserve SQL data volume
|
||||||
docker compose stop opcua # stop a single service (also: ldap, mssql)
|
docker compose stop opcua # stop a single service (also: ldap, mssql, smtp)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full teardown** (removes volumes, optionally images and venv):
|
**Full teardown** (removes volumes, optionally images and venv):
|
||||||
@@ -77,11 +86,11 @@ After a full teardown, the next `docker compose up -d` starts fresh — re-run t
|
|||||||
|
|
||||||
```
|
```
|
||||||
infra/
|
infra/
|
||||||
docker-compose.yml # All three services
|
docker-compose.yml # All four services
|
||||||
teardown.sh # Teardown script (volumes, images, venv)
|
teardown.sh # Teardown script (volumes, images, venv)
|
||||||
glauth/config.toml # LDAP users and groups
|
glauth/config.toml # LDAP users and groups
|
||||||
mssql/setup.sql # Database and user creation
|
mssql/setup.sql # Database and user creation
|
||||||
opcua/nodes.json # Custom OPC UA tag definitions
|
opcua/nodes.json # Custom OPC UA tag definitions
|
||||||
tools/ # Python CLI tools (opcua, ldap, mssql)
|
tools/ # Python CLI tools (opcua, ldap, mssql, smtp)
|
||||||
README.md # Quick-start for the infra folder
|
README.md # Quick-start for the infra folder
|
||||||
```
|
```
|
||||||
|
|||||||
116
test_infra_smtp.md
Normal file
116
test_infra_smtp.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Test Infrastructure: SMTP Server (Mailpit)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The test SMTP server uses [Mailpit](https://mailpit.axllent.org/), a lightweight email testing tool that captures all outgoing emails without delivering them. It provides both an SMTP server for sending and a web UI for inspecting captured messages.
|
||||||
|
|
||||||
|
## Image & Ports
|
||||||
|
|
||||||
|
- **Image**: `axllent/mailpit:latest`
|
||||||
|
- **SMTP port**: 1025
|
||||||
|
- **Web UI / API**: `http://localhost:8025`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Setting | Value | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| `MP_SMTP_AUTH_ACCEPT_ANY` | `1` | Accept any SMTP credentials (or none) — no real authentication |
|
||||||
|
| `MP_SMTP_AUTH_ALLOW_INSECURE` | `1` | Allow auth over plain SMTP (no TLS required) — dev only |
|
||||||
|
| `MP_MAX_MESSAGES` | `500` | Maximum stored messages before oldest are auto-deleted |
|
||||||
|
|
||||||
|
Mailpit accepts all emails regardless of sender/recipient domain. No emails leave the server — they are captured and viewable in the web UI.
|
||||||
|
|
||||||
|
## SMTP Connection Settings
|
||||||
|
|
||||||
|
For `appsettings.Development.json` (Notification Service):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Smtp": {
|
||||||
|
"Server": "localhost",
|
||||||
|
"Port": 1025,
|
||||||
|
"AuthMode": "None",
|
||||||
|
"FromAddress": "scada-notifications@company.com",
|
||||||
|
"ConnectionTimeout": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Since `MP_SMTP_AUTH_ACCEPT_ANY` is enabled, the Notification Service can use any auth mode:
|
||||||
|
- **No auth**: Connect directly, no credentials needed.
|
||||||
|
- **Basic Auth**: Any username/password will be accepted (useful for testing the auth code path without a real server).
|
||||||
|
- **OAuth2**: Not supported by Mailpit. For OAuth2 testing, use a real Microsoft 365 tenant.
|
||||||
|
|
||||||
|
## Mailpit API
|
||||||
|
|
||||||
|
Mailpit exposes a REST API at `http://localhost:8025/api` for programmatic access:
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/api/v1/info` | GET | Server info (version, message count) |
|
||||||
|
| `/api/v1/messages` | GET | List messages (supports `?limit=N`) |
|
||||||
|
| `/api/v1/message/{id}` | GET | Read a specific message |
|
||||||
|
| `/api/v1/messages` | DELETE | Delete all messages |
|
||||||
|
| `/api/v1/search?query=...` | GET | Search messages |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Check the container is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps --filter name=scadalink-smtp
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open the web UI at `http://localhost:8025` to view captured emails.
|
||||||
|
|
||||||
|
3. Send a test email using `curl` or any SMTP client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Python's smtplib (one-liner)
|
||||||
|
python3 -c "
|
||||||
|
import smtplib; from email.mime.text import MIMEText
|
||||||
|
msg = MIMEText('Test body'); msg['Subject'] = 'Test'; msg['From'] = 'test@example.com'; msg['To'] = 'user@example.com'
|
||||||
|
smtplib.SMTP('localhost', 1025).sendmail('test@example.com', ['user@example.com'], msg.as_string())
|
||||||
|
print('Sent')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Tool
|
||||||
|
|
||||||
|
The `infra/tools/smtp_tool.py` script provides a convenient CLI for interacting with the SMTP server and Mailpit API. This tool uses only Python standard library modules — no additional dependencies required.
|
||||||
|
|
||||||
|
**Commands**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check SMTP connectivity and Mailpit status
|
||||||
|
python infra/tools/smtp_tool.py check
|
||||||
|
|
||||||
|
# Send a test email
|
||||||
|
python infra/tools/smtp_tool.py send --to user@example.com --subject "Alarm: Tank High Level" --body "Tank level exceeded 95%"
|
||||||
|
|
||||||
|
# Send with BCC (matches ScadaLink notification delivery pattern)
|
||||||
|
python infra/tools/smtp_tool.py send --to scada-notifications@company.com --bcc "operator1@company.com,operator2@company.com" --subject "Shift Report"
|
||||||
|
|
||||||
|
# List captured messages
|
||||||
|
python infra/tools/smtp_tool.py list
|
||||||
|
|
||||||
|
# Read a specific message by ID
|
||||||
|
python infra/tools/smtp_tool.py read --id <message-id>
|
||||||
|
|
||||||
|
# Clear all messages
|
||||||
|
python infra/tools/smtp_tool.py clear
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--host` and `--port` to override SMTP defaults (localhost:1025), `--api` for the Mailpit API URL. Run with `--help` for full usage.
|
||||||
|
|
||||||
|
## Relevance to ScadaLink Components
|
||||||
|
|
||||||
|
- **Notification Service** — test SMTP delivery, BCC recipient handling, plain-text formatting, and store-and-forward retry behavior (Mailpit can be stopped/started to simulate transient failures).
|
||||||
|
- **Store-and-Forward Engine** — verify buffered retry by stopping the SMTP container and observing queued notifications.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Mailpit does **not** support OAuth2 Client Credentials authentication. To test the OAuth2 code path, use a real Microsoft 365 tenant (see Q12 in `docs/plans/questions.md`).
|
||||||
|
- To simulate SMTP failures for store-and-forward testing, stop the container: `docker compose stop smtp`. Restart with `docker compose start smtp`.
|
||||||
|
- The web UI at `http://localhost:8025` provides real-time message inspection, search, and message source viewing.
|
||||||
|
- No data persistence — messages are stored in a temporary database inside the container and lost on container removal.
|
||||||
Reference in New Issue
Block a user