Send email from any GitHub workflow over a domain you verified with MailKite — release notes, CI alerts, and template sends. And with one small relay, trigger workflows from inbound email.
Docs · CLI guide · Receiving · mailkite.dev
The action wraps the official @mailkite/cli
(npx --yes @mailkite/cli send); template sends use the mailkite
Node SDK. Your API key travels only in the MAILKITE_API_KEY environment variable — never on
a command line, never through a shell.
Add your MailKite API key (create one at app.mailkite.dev) as the
repository secret MAILKITE_API_KEY, then:
name: release-email
on:
release:
types: [published]
jobs:
announce:
runs-on: ubuntu-latest
steps:
- name: Email the release notes
id: email
uses: mailkite/send-email-action@v1
with:
api-key: ${{ secrets.MAILKITE_API_KEY }}
from: releases@mail.yourapp.com # a domain you verified with MailKite
to: team@yourapp.com
subject: "${{ github.event.repository.name }} ${{ github.event.release.tag_name }} is out"
text: ${{ github.event.release.body }}
- name: Log the message id
run: echo "sent ${{ steps.email.outputs.message-id }}"| Input | Required | Description |
|---|---|---|
api-key |
✓ | MailKite API key. Store it as a repository secret; it is passed via env, never argv. |
from |
✓ | Sender address on a domain you verified with MailKite. |
to |
✓ | Recipient address, or a comma-separated list. |
subject |
Required unless template-id supplies one. |
|
html |
HTML body. Provide html, text, or template-id (html + text together is fine). |
|
text |
Plain-text body. | |
template-id |
Send from a saved template (tpl_…) or a base template (base_…). |
|
template-data |
JSON object (as a string) filling the template's {{merge_tags}}, e.g. '{"name":"Ann"}'. |
|
reply-to |
Reply-To address. |
| Output | Description |
|---|---|
message-id |
The MailKite message id (msg_…) of the sent email. |
- uses: mailkite/send-email-action@v1
with:
api-key: ${{ secrets.MAILKITE_API_KEY }}
from: onboarding@mail.yourapp.com
to: ${{ github.event.client_payload.email }}
template-id: tpl_welcome123
template-data: '{"name":"Ann","plan":"pro"}'MailKite receives email for your domain and delivers it as a signed webhook. Point that
webhook at a ~30-line relay that verifies the signature and forwards a summary to GitHub's
repository_dispatch
API — and any email to deploy@mail.yourapp.com (or support@, triage@, …) starts a
workflow. Combined with this action, the workflow can email a reply: a full
email → workflow → email loop.
1. The relay (a Cloudflare Worker; any host that runs the mailkite SDK works):
// relay.js — verify the MailKite webhook, forward a summary to repository_dispatch
import { MailKite } from "mailkite";
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("method not allowed", { status: 405 });
const body = await request.text();
const sig = request.headers.get("x-mailkite-signature");
if (!MailKite.verifyWebhook(sig, body, env.MAILKITE_WEBHOOK_SECRET)) {
return new Response("bad signature", { status: 401 });
}
const email = JSON.parse(body); // { id, from: {address}, to: [{address}], subject, text, html, … }
const res = await fetch(`https://api.github.com/repos/${env.GITHUB_REPO}/dispatches`, {
method: "POST",
headers: {
authorization: `Bearer ${env.GITHUB_TOKEN}`,
accept: "application/vnd.github+json",
"content-type": "application/json",
"user-agent": "mailkite-github-relay",
},
// client_payload allows at most 10 top-level properties — send a summary.
body: JSON.stringify({
event_type: "inbound-email",
client_payload: {
messageId: email.id,
from: email.from.address,
to: email.to[0]?.address ?? null,
subject: email.subject ?? "",
text: (email.text ?? "").slice(0, 8000),
},
}),
});
return new Response(res.ok ? "dispatched" : "github error", { status: res.ok ? 200 : 502 });
},
};# wrangler.toml
name = "mailkite-github-relay"
main = "relay.js"
compatibility_date = "2026-07-01"
compatibility_flags = ["nodejs_compat"] # MailKite.verifyWebhook uses node:crypto
[vars]
GITHUB_REPO = "your-org/your-repo"npm install mailkite wrangler
npx wrangler secret put GITHUB_TOKEN # fine-grained PAT, Contents: read & write on the repo
npx --yes @mailkite/cli secret get # your whsec_… webhook signing secret
npx wrangler secret put MAILKITE_WEBHOOK_SECRET
npx wrangler deploy2. Point your domain's webhook at the relay:
npx --yes @mailkite/cli webhook set <domainId> https://mailkite-github-relay.<account>.workers.dev
npx --yes @mailkite/cli webhook test <domainId> # sends a signed sample event3. The workflow it triggers — including an auto-reply through this action:
name: inbound-email
on:
repository_dispatch:
types: [inbound-email]
jobs:
handle:
runs-on: ubuntu-latest
steps:
- name: Do the work
env: # env indirection — never interpolate email content into shell text
SUBJECT: ${{ github.event.client_payload.subject }}
SENDER: ${{ github.event.client_payload.from }}
run: echo "email from $SENDER — $SUBJECT"
- name: Reply by email
uses: mailkite/send-email-action@v1
with:
api-key: ${{ secrets.MAILKITE_API_KEY }}
from: bot@mail.yourapp.com
to: ${{ github.event.client_payload.from }}
subject: "Re: ${{ github.event.client_payload.subject }}"
text: "Got it — the workflow is running: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"Honest caveats: repository_dispatch only triggers the workflow file on the default
branch; client_payload is capped at 10 top-level properties (hence the summary); and
email content is untrusted input — keep it in env:/with: and out of inline shell text.
Use the major tag (mailkite/send-email-action@v1) or pin an exact release
(@v1.0.0). Development happens in the MailKite monorepo; this repo is the published
action. Issues: MailKite docs.