From 784662c9679df3c8fca491640b2af64648a5a41f Mon Sep 17 00:00:00 2001 From: Tero Date: Sat, 29 Aug 2026 20:26:13 +0300 Subject: [PATCH] Add script for testing smtp --- README.md | 10 ++- TODO.md | 2 +- .../__pycache__/check-smtp.cpython-314.pyc | Bin 0 -> 3772 bytes scripts/check-smtp.py | 57 ++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 scripts/__pycache__/check-smtp.cpython-314.pyc create mode 100644 scripts/check-smtp.py diff --git a/README.md b/README.md index b306f9b..d40414a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,15 @@ Set `PUBLIC_URL` and the `MAIL_*` values in `.env` to send recovery emails. The docker compose up --build -d ``` -5. Send a delivery test to an inbox you control: +5. Confirm the SMTP connection, TLS handshake, and credentials without sending an email: + +```sh +python ./scripts/check-smtp.py +``` + +The command prints `SMTP_CONNECTION_OK` only after the SMTP server accepts the STARTTLS connection and authenticates the configured account. It prints a clear configuration or connection error otherwise and never sends a message. + +6. Send a delivery test to an inbox you control: ```powershell .\scripts\test-smtp.ps1 -To you@example.com diff --git a/TODO.md b/TODO.md index 363a879..d13ede4 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. See the SMTP delivery steps in [README.md](README.md); complete after the test email is received. +- [ ] 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. - [ ] 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 new file mode 100644 index 0000000000000000000000000000000000000000..402e4764586864e682850d9ec9ddff1e86bb0c7e GIT binary patch literal 3772 zcmai0|8EmV7N7O*+UsxY1Scfq3)zs8;24rN&;|m83&aT_BzD)v=?@N;v6p1QvG;b@ zfw*^VeoWd*q3TIZb(NaxerV90?u&HNfpii@>QR1lYEr{Ch&%QEfE*z8+WvBH*526U z5KQ8kH#2YEd$aT2`#dk}v>|9e)_!#94;F;J#}lKOOT?2L5c7yfDmssNVuVl$eKe^i zeI!*P@D%jcNgGceehZ<53!x^|h$ii=Xwuq>4wcj# zB8tQ`!4qwWLz9k4=RuPhCH7)|m7R38BHld7I#7>kk_vF8V-*8$=`|InExPpcrb?MA zJHU#Bh>nuYNF+Sg?8&eoix=m(;8-ZY#nY*j5Ld-nQJv#rsRS3xsMA7972`2gOsBXj zqB@<+0b{A`*U2>cO0O6h?u{-J*qC*X}6I zryTg25qwT>JSrOx5@=(+Jn{r@>McnQV4d5V@nNgb&{qgaM3@Lzx3R#~;KKWcEs1(; z3-H_jpPwfUySBK&<|Z@>g5ZSnIVULN#1-I_+axMX8fT}9XL&eldXaV8_%9(8VGT`z zDuXJI0e6`;s26JpQ!mp0ygi;a?)BVVv$1<<%PB|iAY?_m5pQWj9ws|DCMpVcrY<2h z+r^zqOLIk1xEh<0k^-lu^||hk;ERIHfln6wRS{Gbjw)SQI-_1Za2Ons5mNCqc213G z(}?35u}331#yvL8A}Y~~Vp7l?dH} zWZC`Mu1|J-*7#}ThJ#%?vv{W9XkBx(uC#sO`P}n6?`q~xZ(o1=_rET5pIqxc`Q^cU z_i&+mY^{6jZam-3f9nW7aiZ$}Ctxv(NXbDU7>#g#PPiL^ z8Zh#*As5Eytw|`d74Bik-uo+p+0fI1B6|MEqN>n2B4g@88Pm{yoGv1ekcus^6>7!` zXsj;N1{Ed@xkk7qis9XfQ^UAvw5#a;k_nQHX^UsKp7^!`7N@m9ts%2KhPSdgMgT+8 z^h@mI$^8vw84?+(GiEQoA5T@DGqg$6OHR?1rzH9%+~#Mv%@Ihu#TKZr*;p$hrDSPD zktSn30N-zWYef@>&bt5)vA;n(N<`spGi0=!?wK$XXf5tm_J)_WAlPrK+$qvw3(pv! zqw~B>2B7!$HW4nTMBa-34wdW#fAkvMydy+95bq38(6b>LUVSU2wm>D)2Al}l!e5z8NQFpn)Y*=b!=ed zZ1l807%GtgKOfSZ`s%r$j~^cx^OdH7fnadL&kq77Sa7|oKsu>Iv*_!Pf{?R^8Bl@yCD(fP!G^TK#dKu?0jfv_Y6OC#X zSy0l+SwS;H`Ul+7)3{d6``$b^!utlJL0>2|GCmxXF>uQ`k!a+UpvpKEY1E~(n9@iw zrD~+2B(XT4Qw3Qj>sU$@CP+vG{{UuybNA z-_^Jv=`co1a#6#aqL~!{F$avIh|HPLlI*&%S;icHEKGosDN{6S=E?~ zBFL%OjG!@6Oi`|+<%Eo5K{LmbB3xRdL1tQ3@TD-~6`PDhT%(|POvwX4Vt`Xp1jd66 zcipGVNBsr2XU*-&yS)W>&zifZ;6AeEJ~DrHgK}MSEYGf2?Y&2}KWy*KvlDCWz019S zmv3cnW^cW7^PRg>g%jbWD+^a(eSSE{PCT-(7$uvJ-$>jNZ;E#<`Tjue_?x*#KF^UUzlQpWZfOs|)PDZ`gf# zwsU@XlVa%V4QI_#qHQ?`w^FlWUWu(zE3>%+C%$BEWOKGN_o&eacE^v8y~y0T1|eT~EYB_kcDJE#s6eH{%YcY`F!xQY;Vg z{xMIrW{E=K6OTrVfq`?2j8l#L2IjQj@(LVYk2V=6Ej_MuH^O#cUxeVZ1?Lo1nDJc| zO95pIF<&C zJH&p6Z1DdO)&H|*GKZ=ko2^8McwApkI5*!wlw%=#kKDCsqUfdvZ2c!SzulQcs@xoa7oXfJ-ErU&?mI#*v`?Z~k`H{7=xZZ@nlgK%!S`u&+4)2P2x(@zSn Y1g<*|qZ`(Nz2t48Wq>1Zb2QNZ1DSUJ?f?J) literal 0 HcmV?d00001 diff --git a/scripts/check-smtp.py b/scripts/check-smtp.py new file mode 100644 index 0000000..52f04bb --- /dev/null +++ b/scripts/check-smtp.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Verify SMTP connectivity and authentication without sending email.""" + +import os +import smtplib +import ssl +import sys +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(): + 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)}") + + host = os.environ["MAIL_HOST"] + port = int(os.environ["MAIL_PORT"]) + username = os.environ["MAIL_USERNAME"] + password = os.environ["MAIL_PASSWORD"] + context = ssl.create_default_context() + + try: + with smtplib.SMTP(host, port, timeout=15) as client: + client.ehlo() + client.starttls(context=context) + client.ehlo() + client.login(username, password) + except (OSError, smtplib.SMTPException) as error: + print(f"SMTP_CONNECTION_FAILED: {error}", file=sys.stderr) + return 1 + + print(f"SMTP_CONNECTION_OK: {host}:{port} authenticated as {username}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (RuntimeError, ValueError) as error: + print(f"SMTP_CONFIGURATION_ERROR: {error}", file=sys.stderr) + raise SystemExit(2)