From 88d34a3827526272893595e35e502dc56d019791 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 6 Oct 2026 13:58:59 -0400 Subject: [PATCH] fix(notifications): a line break in a rule name no longer stops the alert email (#5064) The rule name goes into the Subject header, and EmailMessage refuses a header holding a line break, so the whole email failed. The web form can't enter one but MCP takes any string. The subject now folds whitespace. Found by the new From-fallback test in CI run 8349. Co-Authored-By: Claude Opus 5.5 --- steward/core/notifications.py | 3 ++- tests/core/test_notifications.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/steward/core/notifications.py b/steward/core/notifications.py index 1ee6fa4..03dfbec 100644 --- a/steward/core/notifications.py +++ b/steward/core/notifications.py @@ -57,7 +57,8 @@ def email_body(alert: dict) -> str: async def _send_email(cfg: dict, alert: dict) -> dict: try: msg = EmailMessage() - msg["Subject"] = f"[Steward] {alert['state']} — {alert['rule_name']}" + # A header may not hold a line break; a rule name can (MCP takes any string). + msg["Subject"] = " ".join(f"[Steward] {alert['state']} — {alert['rule_name']}".split()) msg["From"] = cfg.get("from") or cfg.get("username") or "steward@localhost" msg["To"] = ", ".join(cfg["recipients"]) msg.set_content(email_body(alert)) diff --git a/tests/core/test_notifications.py b/tests/core/test_notifications.py index 1a6a473..f476341 100644 --- a/tests/core/test_notifications.py +++ b/tests/core/test_notifications.py @@ -106,15 +106,17 @@ def test_smtp_send_connects_by_mode_with_a_timeout(monkeypatch, port, tls, expec assert smtp.calls == expected + [("login", "u", "p"), ("send", "steward@example.com")] -async def test_email_from_address_falls_back_to_username_then_localhost(monkeypatch): +async def test_email_from_falls_back_and_a_line_break_in_the_name_still_sends(monkeypatch): sent = [] - monkeypatch.setattr(n, "_smtp_send_sync", lambda cfg, msg: sent.append(msg["From"])) + monkeypatch.setattr(n, "_smtp_send_sync", lambda cfg, msg: sent.append((msg["From"], msg["Subject"]))) base = {"host": "mx", "port": 587, "recipients": ["a@example.com"]} for cfg in ({**base, "from": "alerts@example.com", "username": "u@example.com"}, {**base, "from": "", "username": "u@example.com"}, {**base, "from": "", "username": ""}): assert (await n._send_email(cfg, ALERT))["sent"] - assert sent == ["alerts@example.com", "u@example.com", "steward@localhost"] + assert [f for f, _ in sent] == ["alerts@example.com", "u@example.com", "steward@localhost"] + # The rule name has a line break; the subject folds it to a space rather than failing. + assert sent[0][1] == '[Steward] FIRING — Disk "root" on web01 \\ backslash' def test_email_body_names_no_data_and_the_note():