diff --git a/README.md b/README.md index d40414a..93ce30d 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,11 @@ The command prints `SMTP_CONNECTION_OK` only after the SMTP server accepts the S 6. Send a delivery test to an inbox you control: -```powershell -.\scripts\test-smtp.ps1 -To you@example.com +```sh +python ./scripts/send-test-email.py --to you@example.com ``` -The command prints `SMTP_TEST_SENT_TO=
` only after the SMTP server accepts the message. Confirm the message arrives, then use **Forgot your password?** in the application to verify a real reset email and link. +The command works on Windows, Linux, and macOS. It prints `SMTP_TEST_SENT_TO=` only after the SMTP server accepts the message. Confirm the message arrives, then use **Forgot your password?** in the application to verify a real reset email and link. The existing PowerShell alternative remains available as `./scripts/test-smtp.ps1 -To you@example.com`. ## Production notes diff --git a/TODO.md b/TODO.md index d13ede4..a3ac9e1 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ - [x] Local accounts, administrator approval, and TOTP MFA. - [x] QR-based MFA enrollment for iPhone and Android authenticators. - [x] Email addresses, member password changes, and expiring password-reset links. -- [ ] Configure production SMTP credentials and verify outgoing email delivery. Run `python ./scripts/check-smtp.py`, then complete after the delivery test in [README.md](README.md) is received. +- [ ] Configure production SMTP credentials and verify outgoing email delivery. Run `python ./scripts/check-smtp.py`, then `python ./scripts/send-test-email.py --to you@example.com`; complete after the delivery test in [README.md](README.md) is received. - [ ] Run `./scripts/rotate-admin.ps1` on Windows or `sh ./scripts/rotate-admin.sh` on Linux/macOS, then sign in as `admin` and scan the new MFA QR code to complete authenticator enrollment. - [x] Add CSRF protection to all state-changing forms. - [ ] Add automated database backups and test restoration. diff --git a/scripts/__pycache__/check-smtp.cpython-314.pyc b/scripts/__pycache__/check-smtp.cpython-314.pyc index 402e476..edc7095 100644 Binary files a/scripts/__pycache__/check-smtp.cpython-314.pyc and b/scripts/__pycache__/check-smtp.cpython-314.pyc differ diff --git a/scripts/__pycache__/send-test-email.cpython-314.pyc b/scripts/__pycache__/send-test-email.cpython-314.pyc new file mode 100644 index 0000000..51577f4 Binary files /dev/null and b/scripts/__pycache__/send-test-email.cpython-314.pyc differ diff --git a/scripts/send-test-email.py b/scripts/send-test-email.py new file mode 100644 index 0000000..f16a326 --- /dev/null +++ b/scripts/send-test-email.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Send one explicit SMTP delivery test email using local .env settings.""" + +import argparse +import os +import smtplib +import ssl +import sys +from datetime import datetime, timezone +from email.message import EmailMessage +from pathlib import Path + +REQUIRED_SETTINGS = ("MAIL_HOST", "MAIL_PORT", "MAIL_USERNAME", "MAIL_PASSWORD", "MAIL_FROM") + + +def load_dotenv(path): + if not path.is_file(): + raise RuntimeError("Missing .env. Copy .env.example to .env and enter the SMTP settings.") + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + key, separator, value = line.partition("=") + if separator and key.strip(): + os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) + + +def main(): + parser = argparse.ArgumentParser(description="Send an Eternity Project SMTP test email.") + parser.add_argument("--to", required=True, help="Recipient email address") + args = parser.parse_args() + + project_root = Path(__file__).resolve().parent.parent + load_dotenv(project_root / ".env") + missing = [setting for setting in REQUIRED_SETTINGS if not os.environ.get(setting)] + if missing: + raise RuntimeError(f"Missing required SMTP settings: {', '.join(missing)}") + + message = EmailMessage() + message["Subject"] = "Eternity Project SMTP verification" + message["From"] = os.environ["MAIL_FROM"] + message["To"] = args.to + message.set_content( + f"SMTP delivery verification completed at {datetime.now(timezone.utc).isoformat()}.\n" + "You can now test password-reset delivery from the application." + ) + + try: + with smtplib.SMTP(os.environ["MAIL_HOST"], int(os.environ["MAIL_PORT"]), timeout=15) as client: + client.ehlo() + client.starttls(context=ssl.create_default_context()) + client.ehlo() + client.login(os.environ["MAIL_USERNAME"], os.environ["MAIL_PASSWORD"]) + client.send_message(message) + except (OSError, smtplib.SMTPException) as error: + print(f"SMTP_TEST_FAILED: {error}", file=sys.stderr) + return 1 + + print(f"SMTP_TEST_SENT_TO={args.to}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except RuntimeError as error: + print(f"SMTP_CONFIGURATION_ERROR: {error}", file=sys.stderr) + raise SystemExit(2)