action-send-mail is a GitHub Action for one job: SMTP mail to several recipients, with the envelope details handled for you
:gear: GitHub Action for sending emails
At a glance
- What is it?
- dawidd6/action-send-mail is a JavaScript GitHub Action that sends mail over SMTP, with plain text, HTML, multipart and Markdown bodies, file attachments, cc, bcc, custom headers, and a connection_url input that encodes the TLS mode in the scheme. It depends on nodemailer and showdown, and its most useful behaviour is the way it fills in a missing SMTP envelope address.
- Who is it for?
- Use action-send-mail if you already run a workflow on GitHub Actions and already have SMTP credentials, and you need a notification email to several people as one step in a job. Do not expect a mailing service, a template engine, a retry policy or delivery reporting, because the action hands the message to nodemailer and stops there.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A mail step, not a mailing list
The action is described in one line: an action that simply sends a mail to multiple recipients. That smallness is the design, not a limitation of ambition.
It is a GitHub Action, so it runs inside a job on a runner, and its whole job is to take inputs and send one message. There is no subscriber list, no template system, no campaign management and no delivery dashboard. The audience for it is a team that wants a job result emailed to a handful of people, or a release note mailed out, without standing up a service.
The implementation is a single JavaScript entry point. The repository root holds action.yml, main.js, package.json, package-lock.json and a renovate.json, and the package is an ES module with a type of module and main of main.js. The runtime dependencies are small and tell you what the action actually does: @actions/core for the action inputs, @actions/glob for expanding attachment patterns, nodemailer for the SMTP conversation, and showdown for the Markdown to HTML conversion.
The features list is what a job notification needs and nothing more. A plain text body, an HTML body, a multipart body with both, Markdown converted to HTML, and file attachments with globbing support.
The Markdown conversion has one rule that the documentation states explicitly: use html_body, not body, when you want Markdown converted. That is the kind of detail that costs an afternoon if you do not read it, and it is a fair example of how the README is written, with the sharp edges called out rather than left to be discovered.
The connection URL encodes the TLS mode in its scheme
The most interesting input is connection_url, and the reason it exists is that the older way of configuring SMTP had four separate fields that had to agree with each other.
The documentation describes connection_url as specifying the connection via URL, and says it replaces server_address, server_port, username and password. The formats are listed in the comment block in the usage example:
- name: Send mail
uses: dawidd6/action-send-mail@<REF>
env:
# Optional proxy, NO_PROXY is honored too:
SMTP_PROXY: http://proxy.example.test:3128
with:
# Specify connection via URL (replaces server_address, server_port,
# username and password)
#
# Format:
#
# * smtp://user:password@server:port
# * smtps://user:password@server:port (TLS, default port 465)
# * smtp+starttls://user:password@server:port (STARTTLS required, default port 587)
connection_url: ${{secrets.MAIL_CONNECTION}}So smtp is plain, smtps is implicit TLS on the default port 465, and smtp+starttls is STARTTLS on the default port 587. The scheme decides the encryption mode, which removes a class of misconfiguration where the port and the secure flag disagree.
The individual fields still work if you prefer them, and the usage example shows server_address and server_port with a default of 25, and a secure flag that defaults to true when the server port is 465.
Two of the security related inputs deserve attention. require_tls, when set, aborts the connection if the server does not support or negotiate STARTTLS, instead of silently falling back to a plain text connection, and the documentation recommends it whenever secure is false and you authenticate with a username and password. That is the correct default for a CI environment, where a silent downgrade means your credentials cross the network in the clear. And ignore_cert accepts unsigned or invalid certificates, which is a debugging convenience and a real risk, so it belongs in a development workflow and not in a pipeline that carries production credentials.
The envelope trick, and why it saves you a field
Nodemailer distinguishes the visible message headers from the SMTP envelope, and this action spends a section of the documentation making the difference usable.
The remark for envelope_from and envelope_to explains the constraint: nodemailer requires that if the optional custom envelope is used, both its attributes from and to must be set. To let you set only one of them, the action fills in the other. If only envelope_from is set, envelope_to is set to the concatenation of to, cc and bcc with duplicates removed. If only envelope_to is set, envelope_from is set to the address specified in from.
That is a small piece of convenience with a real purpose. The envelope addresses are the ones the receiving mail server logs and rejects against, and they are not always the same as the From and To headers. A mailing list or a ticketing system that needs a single envelope recipient while showing a different To in the message would otherwise have to set both fields by hand and keep them in sync.
The inputs involved are otherwise familiar. envelope_from overrides the username, and envelope_to overrides to, cc and bcc, with multiple addresses separated by commas. The visible message fields are to, cc, bcc, reply_to and in_reply_to, and the from field is required, with supported formats including a display name in angle brackets.
There is a YAML trap here that the documentation flags. Mail addresses can contain YAML special characters like the angle brackets used for display names, and to avoid parsing issues, addresses containing such characters should be enclosed in single quotes. The address formats themselves are the ones nodemailer's addressparser supports, which is the same parser the action now uses.
Bodies, attachments, headers and the other knobs
The rest of the input list is a working set of mail options, and it is worth taking them in groups because they are each small.
The bodies come in four shapes. body is a plain text body, html_body is an HTML body read from a file with the file:// scheme, a multipart body combines plain text and HTML, and convert_markdown turns the Markdown body into HTML, with the note that you should also set content_type to text/html when you do.
Attachments are given as a comma separated list that supports globbing, so attachments.zip, git.diff and ./dist/static/*.js are all valid in one run. The @actions/glob dependency is there for exactly that.
Then there is the operational set. priority accepts high, normal or low. headers takes a JSON object of custom headers, and it can overwrite the defaults nodemailer would otherwise set. nodemailerlog enables the underlying library's logging, and nodemailerdebug enables both that and the debug mode.
The subject and the body interpolate workflow context directly, with the example using Build job of ${{github.repository}} completed successfully! as the plain text body. That is the typical use: a job that ends and mails the result to the people who care.
What is absent is as instructive as what is present. There is no retry configuration, no template handling, no scheduling, no suppression list and no delivery status callback, because the action sends a message and exits. If your workflow needs any of those, this is the wrong tool and the mistake will surface as a lost email rather than as an error.
YAML quoting, Gmail app passwords and self-hosted runners
The troubleshooting section covers the two failure modes people actually hit.
The first is Gmail. The instruction is to use an App password instead of your normal Google password, and creating one requires two steps: enabling 2-Step Verification, then creating an App password for Mail. That is Google's own requirement, and it is the single most common cause of a first failed run.
The second is the unauthenticated login case. The documentation explains that username and password are optional in order to support self-hosted runners reaching on-premise infrastructure, and warns that if you are accessing public email servers you should make sure you provide username and password authentication through GitHub Secrets so the email delivery is secure.
That warning is the one to take seriously. An action that can send mail with no credentials is extremely convenient inside a company network where the relay does not require authentication, and it is a genuine finding for anyone who installs this action in a shared runner environment. The credentials belong in repository secrets, and the documentation points at the GitHub Secrets documentation for that.
The repository also lists an optional SMTP_PROXY environment variable, with the note that NO_PROXY is honored too, which is what you need when the runner sits behind a corporate proxy.
A detail that tells you about the project's tooling: the repository commits a node_modules/ directory, and it uses renovate.json to keep dependencies current. The action is pinned by version tag in the workflow, with the usage example using a placeholder reference rather than a specific tag, which is the right thing to do but leaves the choice to whoever writes the workflow.
SMTP against your own server, or an API against a provider
There is a real alternative to this action, and the difference is architectural rather than cosmetic.
The alternative is a hosted transactional email provider reached over HTTPS, where the action would talk to that provider's API instead of speaking SMTP. The trade-off is straightforward. With this action, the sending is entirely under your control: you point it at your own relay, you hold the credentials, and nothing leaves your infrastructure except the message itself. The cost is that you operate or depend on a mail server, you deal with its reputation, its rate limits and its spam classification, and you have to get TLS and authentication right.
With a provider, the action makes an HTTPS request with an API key, the provider handles deliverability, retries and bounce handling, and the message is no longer under your control. The cost is a per message charge, a vendor dependency, and your content leaving your infrastructure.
The choice depends on the workflow, not on the tool. A pipeline that already has a working relay and just needs to notify three people is well served by an action that speaks SMTP and nothing else. A product that sends mail to end users, with domain reputation requirements and a need for delivery telemetry, needs something with feedback loops, and this action has none.
A third path is the one many teams take without thinking about it: do not send mail from CI at all, and post the result to a chat webhook instead, where the message is a structured payload rather than a formatted email. That loses the attachment support and the rich body, and it gains a durable record of what was said.
MIT terms, a v22 tag and a dependency tree checked in
The licence is MIT, so the action can be vendored, modified and used inside a closed source workflow without a licensing discussion.
The release tags are the interesting operational detail. The published versions are v22 from 2026-09-15, v21 from 2026-09-10 and v20 from 2026-09-08, so three releases in a week, with the version number used directly as the tag. The usage example writes the reference as a placeholder, which tells you that pinning to a specific tag is the intended practice, and the major version bump cadence suggests the author treats breaking input changes as a major release.
The repository is not archived and the last push was on 2026-10-22, so the action is being maintained.
Two structural details are worth knowing before you vendor it. The node_modules/ directory is committed to the repository, which is unusual but is a normal pattern for GitHub Actions that need their dependencies present at runtime without an install step. And renovate.json is present, so the dependency updates are automated, which is the mechanism behind a fast major version cadence.
The dependency set is small enough to audit in a few minutes: @actions/core, @actions/glob, nodemailer and showdown. The security surface of the action is therefore nodemailer plus showdown, and the things to check in your own review are the connection handling and the ignore_cert path, since those are the two places where the action decides whether your credentials travel in the clear.
Editorial conclusion
Use action-send-mail if you already run a workflow on GitHub Actions and already have SMTP credentials, and you need a notification email to several people as one step in a job. Do not expect a mailing service, a template engine, a retry policy or delivery reporting, because the action hands the message to nodemailer and stops there. Before wiring it into a pipeline, decide which TLS mode your server supports, set require_tls rather than relying on a silent fallback, and read the Gmail section, because a normal Google account password will not work and an App password behind two factor authentication is required.
Frequently asked questions
What does the action-send-mail GitHub Action do?
It sends an email to multiple recipients from a GitHub Actions job, supporting a plain text body, an HTML body, a multipart body, Markdown to HTML conversion and file attachments with globbing. It is implemented with nodemailer for sending and showdown for Markdown conversion.
How do I configure the SMTP connection in action-send-mail?
You can either give a connection_url, which replaces server_address, server_port, username and password, or set those fields separately. The URL scheme selects the encryption mode, with smtp for plain, smtps for TLS on the default port 465 and smtp+starttls for STARTTLS on the default port 587.
Why does action-send-mail need both envelope_from and envelope_to?
Nodemailer requires that both attributes be set when a custom envelope is used. The action fills in the missing one, setting envelope_to to the concatenation of to, cc and bcc with duplicates removed, or envelope_from to the address in from.
How do I convert Markdown to HTML in action-send-mail?
Set convert_markdown to true and use the html_body input rather than body, as the documentation states. You should also set content_type to text/html when you convert the body.
How do I send mail from Gmail with action-send-mail?
Use an App password rather than your normal Google password. Creating one requires enabling 2-Step Verification first, and then creating an App password for Mail in the Gmail troubleshooting section of the documentation.
Does action-send-mail support unauthenticated SMTP?
The username and password inputs are optional so that self-hosted runners can reach on-premise infrastructure. The documentation warns that if you are accessing public email servers you should provide username and password authentication through GitHub Secrets.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dawidd6-action-send-mail)Was this useful?