From 983414f2782b49984311794f40aecc276b616bbd Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Mon, 19 Oct 2026 09:10:52 +0000 Subject: [PATCH] fix(mail-templates): read \n as a line break in one-line inputs (1.0.2) The playbook editor's input field is a single , which drops the newlines of a pasted text: the whole description reached the script as one line starting with ## and rendered as a single heading. description and callout_text without a real line break now take the two characters \n as one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJu1jvaug4qY1kMgz8wx41 --- integrations/mail-templates/manifest.yaml | 6 +++--- .../mail-templates/scripts/render_mail_template.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/integrations/mail-templates/manifest.yaml b/integrations/mail-templates/manifest.yaml index 25ce07a..4cc3e6a 100644 --- a/integrations/mail-templates/manifest.yaml +++ b/integrations/mail-templates/manifest.yaml @@ -1,8 +1,8 @@ id: mail_templates name: Mail Templates (ACKnowledge) -version: 1.0.1 +version: 1.0.2 description: "Turns a subject and a markdown description into an ACKnowledge SOC email (dark theme, header badge, signature with the logo, footer) — nothing is sent: chain the output into exchange-send-mail or mail-sender (`subject` → subject, `html_body` → html_body, `text_body` → body). Eight templates, each with its own badge and colour: sla_breach, incident_notification (colour follows `severity`), incident_update, incident_closure, action_required, escalation, threat_bulletin, info. The description takes markdown: headings (## is the blue capitalised section title), bold, italic, links, lists, quotes, rules, GFM tables, `code` and ``` blocks (hashes, IPs, command lines), and level pills `{{critical}}` `{{high}}` `{{medium}}` `{{low}}` `{{info}}` `{{ok}}` — `{{high:Élevée}}` changes the label. Raw HTML in the inputs is escaped, so alert data can be interpolated safely. The logo is an inline data-URI PNG: exchange-send-mail and mail-sender embed it; the Gmail integration does not, and the logo is lost there. No connection, no secret; stdlib-only." -changelog: "1.0.1 — The Test button failed (NameError: __file__) and render-mail-template printed nothing: Riposte runs a script with exec(), without __file__ or __name__. The test no longer imports the renderer and the renderer runs main() unconditionally. | 1.0.0 — Initial release: render-mail-template (8 templates, markdown description with tables, code and level pills, optional callout, action button, case reference and severity, SOC or client footer, French or English) and a self-test." +changelog: "1.0.2 — The playbook editor's input field is a single line and drops pasted newlines, so a pasted description rendered as one heading: description and callout_text without a line break now read the two characters \\n as one. | 1.0.1 — The Test button failed (NameError: __file__) and render-mail-template printed nothing: Riposte runs a script with exec(), without __file__ or __name__. The test no longer imports the renderer and the renderer runs main() unconditionally. | 1.0.0 — Initial release: render-mail-template (8 templates, markdown description with tables, code and level pills, optional callout, action button, case reference and severity, SOC or client footer, French or English) and a self-test." category: email # Nothing to connect to. The only setting names the bot in the SOC footer @@ -27,7 +27,7 @@ commands: enum: [sla_breach, incident_notification, incident_update, incident_closure, action_required, escalation, threat_bulletin, info] description: "Layout: sla_breach (red, SLA ALERT), incident_notification (colour of `severity`), incident_update (blue, STATUS UPDATE), incident_closure (green, CLOSED), action_required (orange), escalation (magenta), threat_bulletin (violet), info (grey, default)" subject: { type: string, description: "Subject, also shown as the title of the mail" } - description: { type: string, description: "Body in markdown — ## section titles, **bold**, lists, tables, `code`, ``` blocks, {{medium}} pills" } + description: { type: string, description: "Body in markdown — ## section titles, **bold**, lists, tables, `code`, ``` blocks, {{medium}} pills. In a one-line field, write each line break as \\n" } callout_text: { type: string, description: "Highlighted box above the description (markdown). Empty: no box" } callout_title: { type: string, description: "Title of the box (default: the template badge, e.g. ⚠ SLA ALERT)" } button_label: { type: string, description: "Action button text, e.g. Open the case in Riposte (needs button_url)" } diff --git a/integrations/mail-templates/scripts/render_mail_template.py b/integrations/mail-templates/scripts/render_mail_template.py index 1c5e7fe..fbeb79f 100644 --- a/integrations/mail-templates/scripts/render_mail_template.py +++ b/integrations/mail-templates/scripts/render_mail_template.py @@ -405,6 +405,16 @@ def _when(raw, lang): return "%d %s %d, %s UTC" % (d.day, STRINGS["en"]["months"][d.month - 1], d.year, d.strftime("%H:%M")) +def _lines(v): + """The markdown with its line breaks. The playbook editor's input field is a + single line and drops pasted newlines, so a text without one may spell them + as the two characters \\n instead.""" + v = str(v or "").strip() + if "\n" not in v and "\\n" in v: + v = v.replace("\\r\\n", "\n").replace("\\n", "\n") + return v + + def _choice(inputs, key, allowed, default): v = _s(inputs.get(key)).lower() or default if v not in allowed: @@ -417,7 +427,7 @@ def render(cfg, inputs): lang = _choice(inputs, "lang", ["fr", "en"], "fr") audience = _choice(inputs, "audience", ["soc", "client"], "soc") subject = _s(inputs.get("subject")) - description = str(inputs.get("description") or "").strip() + description = _lines(inputs.get("description")) if not subject: raise Exception("subject is required") if not description: @@ -435,7 +445,7 @@ def render(cfg, inputs): raise Exception("button_url must start with http:// or https://") if button_label and not button_url: raise Exception("button_url is required with button_label") - callout_text = str(inputs.get("callout_text") or "").strip() + callout_text = _lines(inputs.get("callout_text")) bot = _s(cfg.get("bot_name")) or "Riposte" t = TEMPLATES[template]