134 lines
3.6 KiB
Python
134 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
smtp_send.py
|
||
|
||
Send a plain-text alert message via direct SMTP to the PSI mail infrastructure.
|
||
This script deliberately does NOT use local sendmail.
|
||
|
||
Design goals:
|
||
- Explicit SMTP connection to smtp.psi.ch
|
||
- No SMTP authentication (assumes trusted internal network)
|
||
- Fixed default sender address (can be overridden)
|
||
- Predictable exit codes and errors for use from shell scripts
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import sys
|
||
import smtplib
|
||
from email.message import EmailMessage
|
||
from email.utils import formatdate
|
||
from typing import List
|
||
|
||
|
||
DEFAULT_SMTP_SERVER = "smtp.psi.ch"
|
||
DEFAULT_SMTP_PORT = 25
|
||
DEFAULT_TIMEOUT_S = 10.0
|
||
DEFAULT_FROM_ADDR = "cSAXS@psi.ch"
|
||
DEFAULT_SUBJECT = "EPICS alert"
|
||
|
||
|
||
def build_message(
|
||
*, from_addr: str, to_addrs: List[str], subject: str, body: str
|
||
) -> EmailMessage:
|
||
msg = EmailMessage()
|
||
msg["From"] = from_addr
|
||
msg["To"] = ", ".join(to_addrs)
|
||
msg["Subject"] = subject
|
||
msg["Date"] = formatdate(localtime=True)
|
||
msg.set_content(body)
|
||
return msg
|
||
|
||
|
||
def send_email_via_smtp(
|
||
*,
|
||
to_addrs: List[str],
|
||
body: str,
|
||
subject: str = DEFAULT_SUBJECT,
|
||
from_addr: str = DEFAULT_FROM_ADDR,
|
||
smtp_server: str = DEFAULT_SMTP_SERVER,
|
||
smtp_port: int = DEFAULT_SMTP_PORT,
|
||
timeout_s: float = DEFAULT_TIMEOUT_S,
|
||
) -> None:
|
||
msg = build_message(
|
||
from_addr=from_addr, to_addrs=to_addrs, subject=subject, body=body
|
||
)
|
||
|
||
with smtplib.SMTP(smtp_server, smtp_port, timeout=timeout_s) as smtp:
|
||
# Explicit envelope sender/recipients (avoid relying on headers for SMTP routing)
|
||
smtp.send_message(msg, from_addr=from_addr, to_addrs=to_addrs)
|
||
|
||
|
||
def parse_args(argv: List[str]) -> argparse.Namespace:
|
||
p = argparse.ArgumentParser(
|
||
description="Send a plain-text alert email via direct SMTP (no local sendmail)."
|
||
)
|
||
p.add_argument(
|
||
"--to",
|
||
dest="to_addrs",
|
||
action="append",
|
||
required=True,
|
||
help="Recipient address. Repeat --to for multiple recipients.",
|
||
)
|
||
p.add_argument(
|
||
"message",
|
||
nargs="?",
|
||
default="-",
|
||
help="Message body. Use '-' (default) to read the body from stdin.",
|
||
)
|
||
p.add_argument("--subject", default=DEFAULT_SUBJECT, help="Email subject.")
|
||
p.add_argument(
|
||
"--from-addr", default=DEFAULT_FROM_ADDR, help="Envelope/header From address."
|
||
)
|
||
p.add_argument(
|
||
"--server", default=DEFAULT_SMTP_SERVER, help="SMTP server hostname."
|
||
)
|
||
p.add_argument(
|
||
"--port", type=int, default=DEFAULT_SMTP_PORT, help="SMTP server port."
|
||
)
|
||
p.add_argument(
|
||
"--timeout",
|
||
type=float,
|
||
default=DEFAULT_TIMEOUT_S,
|
||
help="SMTP timeout (seconds).",
|
||
)
|
||
return p.parse_args(argv)
|
||
|
||
|
||
def main(argv: List[str]) -> int:
|
||
args = parse_args(argv)
|
||
|
||
if args.message == "-":
|
||
body = sys.stdin.read()
|
||
else:
|
||
body = args.message
|
||
|
||
# Make the "empty message" case explicit: it’s almost always a bug in alerting.
|
||
if not body.strip():
|
||
print("ERROR: message body is empty", file=sys.stderr)
|
||
return 2
|
||
|
||
try:
|
||
send_email_via_smtp(
|
||
to_addrs=args.to_addrs,
|
||
body=body,
|
||
subject=args.subject,
|
||
from_addr=args.from_addr,
|
||
smtp_server=args.server,
|
||
smtp_port=args.port,
|
||
timeout_s=args.timeout,
|
||
)
|
||
except (OSError, smtplib.SMTPException) as e:
|
||
print(
|
||
f"ERROR: failed to send email via {args.server}:{args.port}: {e}",
|
||
file=sys.stderr,
|
||
)
|
||
return 1
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main(sys.argv[1:]))
|