From 4ca48c76b4fbb3994e1b664f3ffd9cbc5a9b058d Mon Sep 17 00:00:00 2001 From: Fritz Elfert Date: Fri, 13 Mar 2026 10:51:21 +0100 Subject: [PATCH] Fix 234 (#271) * Fix envelope handling * Updated documentation * Update main.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Implemented review suggestions * Implemented review suggestions * Implemented review suggestions * Use nodemailer's addressparser instead of regular expressions * Updated README regarding address formats * Updated README regarding address formats * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Use addressparser regardless of envelopeX ist set or not. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 56 ++++++++++++++++++++++++++++-------- main.js | 85 +++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index afe449b6..45460203 100644 --- a/README.md +++ b/README.md @@ -27,54 +27,86 @@ Some features: # * smtp://user:password@server:port # * smtp+starttls://user:password@server:port connection_url: ${{secrets.MAIL_CONNECTION}} + # Required mail server address if not connection_url: server_address: smtp.gmail.com + # Server port, default 25: server_port: 465 + # Optional whether this connection use TLS (default is true if server_port is 465) secure: true + # Optional (recommended) mail server username: username: ${{secrets.MAIL_USERNAME}} + # Optional (recommended) mail server password: password: ${{secrets.MAIL_PASSWORD}} + # Required mail subject: subject: Github Actions job result - # Optional recipients' addresses: - to: obiwan@example.com,yoda@example.com - # Required sender (Either: "Plain Simple Name " or just "user@doma.in" (without the <>)) - # Important: '<' and '>' are special chars in yaml. Therefore this string should be quoted + + # Optional recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace): + to: obiwan@example.com, yoda@example.com + + # Required sender (supported formats: see "Supported address formats" below) from: 'Luke Skywalker ' + # Optional plain body: body: Build job of ${{github.repository}} completed successfully! + # Optional HTML body read from file: html_body: file://README.html - # Optional carbon copy recipients: - cc: kyloren@example.com,leia@example.com - # Optional blind carbon copy recipients: - bcc: r2d2@example.com,hansolo@example.com + + # Optional carbon copy recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace): + cc: 'kyloren@example.com, "Her Majesty, Princess Leia" ' + + # Optional blind carbon copy recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace): + bcc: r2d2@example.com, hansolo@example.com + # Optional recipient of the email response: reply_to: luke@example.com + # Optional Message ID this message is replying to: - in_reply_to: + in_reply_to: '<3cc627c8-6181-453b-d90b-04aae9e23b21@github.com>' + # Optional unsigned/invalid certificates allowance: ignore_cert: true + # Optional converting Markdown to HTML (set content_type to text/html too): convert_markdown: true + # Optional attachments: attachments: attachments.zip,git.diff,./dist/static/*.js + # Optional priority: 'high', 'normal' (default) or 'low' priority: low + # Optional custom headers: headers: '{"X-Priority": "3 (Normal)", "X-My-Header": "MyValue"}' + # Optional nodemailerlog: true/false nodemailerlog: false - # Optional nodemailerdebug: true/false if true lognodem will also be set true + + # Optional nodemailerdebug: true/false if true nodemailerlog will also be set true nodemailerdebug: false + # Optional custom SMTP MAIL FROM address (overrides username): envelope_from: mailer@example.com - # Optional custom SMTP RCPT TO addresses (overrides to, cc, bcc): - envelope_to: mailer@example.com,admin@example.com + + # Optional custom SMTP RCPT TO addresses (overrides to, cc, bcc). Separate multiple addresses by a comma (possibly surrounded by whitespace): + envelope_to: mailer@example.com, admin@example.com ``` +### Remark for `envelope_from` and `envelope_to` + +[nodemailer](https://nodemailer.com/) (the node module that does the actual sending) requires that if the optional custom envelope is used, **both** its attributes `from` and `to` must be set. To facilitate setting only one of `envelope_from` or `envelope_to`, this action sets the other one from the regular message fields in the following way: + +* If only `envelope_from` is set, `envelope_to` will be set to the concatenation of `to`, `cc` and `bcc` (with duplicates removed). +* If only `envelope_to` is set, `envelope_from` will be set to the address specified in `from`. + +### Supported address formats +This action now uses nodemailer's addressparser. The supported address formats are described [here](https://nodemailer.com/message/addresses). +Mail addresses can contain YAML special characters like '<' and '>'. To avoid YAML parsing issues, addresses that contain such characters should be enclosed in single quotes. ## Troubleshooting diff --git a/main.js b/main.js index 0e0470e1..1c59778d 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,5 @@ import nodemailer from "nodemailer"; +import addressparser from "nodemailer/lib/addressparser/index.js"; import * as core from "@actions/core"; import * as glob from "@actions/glob"; import fs from "node:fs"; @@ -39,6 +40,39 @@ function sleep(ms) { }); } +/** + * Prepare an envelope object for nodemailer. + * + * If only one of envelopeFrom or envelopeTo is set, make sure that both + * are set in the returned object. Furthermore, make sure that the attribute 'to' + * is an array of email addresses, not a comma-separated string. + */ +function setupEnvelope(envelopeFrom, envelopeTo, from, to, cc, bcc) { + if (envelopeFrom || envelopeTo) { + // Take address in from, if envelopeFrom is not set. + envelopeFrom = envelopeFrom ? addressparser(envelopeFrom) : addressparser(from); + if (envelopeFrom.length != 1 || envelopeFrom[0].address == '') { + throw new Error("'envelopeFrom' address is invalid"); + } + if (envelopeTo) { + envelopeTo = addressparser(envelopeTo); + } else { + // Take addresses in to, cc and bcc. Deduplication is handled by nodemailer. + for (const src of [to, cc, bcc]) { + if (src) { + let parsed = addressparser(src); + envelopeTo = envelopeTo ? envelopeTo.concat(parsed) : parsed; + } + } + } + return { + from: envelopeFrom, + to: envelopeTo, + }; + } + return undefined; +} + async function main() { try { let serverAddress = core.getInput("server_address"); @@ -111,7 +145,8 @@ async function main() { // Basic check for an email sender address // Either: "Plain Simple Name " or just "user@doma.in" (without the <>) - if (!(/^([^<>@\s]+\s+)+<[^@\s>]+@[^@\s>]+>$/.test(from) || /^[^<>@\s]+@[^@\s<>]+$/.test(from))) { + let parsed = addressparser(from); + if (parsed.length != 1 || parsed[0].address == '') { throw new Error("'from' address is invalid"); } @@ -148,35 +183,31 @@ async function main() { proxy: process.env.HTTP_PROXY, }); + const messageOptions = { + from: from, + to: to, + subject: getText(subject, false), + cc: cc ? cc : undefined, + bcc: bcc ? bcc : undefined, + replyTo: replyTo ? replyTo : undefined, + inReplyTo: inReplyTo ? inReplyTo : undefined, + references: inReplyTo ? inReplyTo : undefined, + text: body ? getText(body, false) : undefined, + html: htmlBody + ? getText(htmlBody, convertMarkdown) + : undefined, + priority: priority ? priority : undefined, + headers: headers ? JSON.parse(headers) : undefined, + attachments: attachments + ? await getAttachments(attachments) + : undefined, + envelope: setupEnvelope(envelopeFrom, envelopeTo, from, to, cc, bcc), + }; + let i = 1; while (true) { try { - const info = await transport.sendMail({ - from: from, - to: to, - subject: getText(subject, false), - cc: cc ? cc : undefined, - bcc: bcc ? bcc : undefined, - replyTo: replyTo ? replyTo : undefined, - inReplyTo: inReplyTo ? inReplyTo : undefined, - references: inReplyTo ? inReplyTo : undefined, - text: body ? getText(body, false) : undefined, - html: htmlBody - ? getText(htmlBody, convertMarkdown) - : undefined, - priority: priority ? priority : undefined, - headers: headers ? JSON.parse(headers) : undefined, - attachments: attachments - ? await getAttachments(attachments) - : undefined, - envelope: - envelopeFrom || envelopeTo - ? { - from: envelopeFrom ? envelopeFrom : undefined, - to: envelopeTo ? envelopeTo : undefined, - } - : undefined, - }); + const info = await transport.sendMail(messageOptions); break; } catch (error) { if (!error.message.includes("Try again later,")) {