slack-to-email
Searches the connected Slack workspace for direct messages and @mentions directed at the user since the last run, then sends a summary email for each conversation or channel via Google Apps Script. Use this skill whenever the user says "check my Slack", "send me my Slack messages", "forward Slack to email", "Slack notifications to email", "email me my Slack DMs", "what did I miss on Slack", or anything that sounds like they want Slack messages or mentions forwarded to their inbox. Also trigger if the user mentions Slack-to-email, Slack digest, or Slack notification forwarding. This skill is designed to run both manually and as a scheduled task.
Slack-to-Email Forwarder
This skill finds Slack DMs and @mentions you've received since the last run and emails you a summary of each conversation so nothing falls through the cracks.
How it works at a glance
- Verify that Slack and the Apps Script endpoint are both reachable.
- Read
last-run.mdin the working directory to find the cutoff timestamp. - Search Slack for new DMs (1:1 and group) to you and @mentions of you.
- Group messages by conversation (DM) or channel (@mention).
- Send one email per conversation/channel via the Apps Script endpoint.
- Update
last-run.mdwith the current time.
The skill is fully automated — it never prompts the user for confirmation mid-run, because it only reads Slack and sends email to the user's own address. Nothing is deleted or sent to third parties.
Prerequisites
Before the skill can run, the user needs three things in place:
-
Slack MCP connector — The Slack tools (
slack_search_public_and_private,slack_read_channel,slack_read_user_profile) must be available. -
Google Apps Script deployment — The bundled
scripts/gmail-calendar-actions.gsmust be deployed as a web app. Seereferences/apps-script-setup.mdfor step-by-step instructions. -
Config file — A file called
slack-to-email-config.jsonmust exist in the working directory with these fields filled in:
{
"apps_script_url": "https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec",
"apps_script_secret": "YOUR_SECRET_HERE",
"user_email": "[email protected]",
"slack_user_id": "U0YOUR_ID"
}
The slack_user_id is the user's Slack member ID (starts with "U"). If the
user doesn't know theirs, you can find it by calling slack_read_user_profile
with no arguments — it returns the current authenticated user's profile
including their user ID.
Step-by-step instructions
Step 1 — Preflight checks
Read slack-to-email-config.json from the working directory. Verify that all
four required fields are present and not placeholder values: apps_script_url,
apps_script_secret, user_email, and slack_user_id. If the file is missing
or any field is empty/placeholder, stop and tell the user what needs to be
configured. Point them to references/apps-script-setup.md if the Apps Script
fields are the problem.
Verify the Slack connector is working by making a lightweight call — for
example, slack_read_user_profile with no arguments. If it fails, tell the
user the Slack connector isn't connected and stop.
Verify the Apps Script endpoint is reachable by running:
curl -s -L "<apps_script_url>"
The -L flag is important — Google Apps Script always responds with a 302
redirect first, and curl needs to follow it to reach the actual endpoint.
You should get back a JSON response with "status": "ok". If this fails,
tell the user the Apps Script deployment isn't responding and stop.
If all checks pass, proceed.
Step 2 — Determine the time window
Read last-run.md from the working directory. It contains a single ISO 8601
timestamp (e.g., 2026-02-27T15:30:00Z) representing when the skill last
ran successfully.
If last-run.md doesn't exist or is empty, default to 24 hours ago.
Record the current UTC time now — you'll write this to last-run.md at the
end if everything succeeds.
Convert the cutoff timestamp to a Unix timestamp (seconds) for use with
Slack's after parameter.
Step 3 — Search Slack for new messages
Run three searches to find everything relevant since the cutoff:
Search A — @mentions in channels:
Use slack_search_public_and_private with:
query:<@SLACK_USER_ID>(the user's Slack member ID from config, wrapped in angle brackets — this is how Slack represents @mentions)after: the cutoff Unix timestampcontent_types:"messages"include_context:false(we just need the messages themselves)sort:"timestamp"sort_dir:"asc"
Page through all results using the cursor if there's a nextPageToken.
Search B — Direct messages (1:1):
Use slack_search_public_and_private with:
query:to:meafter: the cutoff Unix timestampchannel_types:"im"content_types:"messages"include_context:falsesort:"timestamp"sort_dir:"asc"
Page through all results.
Search C — Group direct messages (multi-person DMs):
Use slack_search_public_and_private with:
query:to:meafter: the cutoff Unix timestampchannel_types:"mpim"content_types:"messages"include_context:falsesort:"timestamp"sort_dir:"asc"
Page through all results. The mpim channel type covers group DMs — messages
sent to you and one or more other people. These are distinct from im (1:1
DMs) and must be searched separately.
Filtering DMs: The to:me searches (both B and C) may return messages you
sent yourself. Filter these out — only keep messages where the sender's user ID
is not the user's own slack_user_id from the config. The search results
include the sender's user ID for each message, so this is straightforward.
Deduplication: If a message appears in multiple searches (e.g., someone @mentioned you in a DM or group DM), deduplicate by message timestamp + channel ID.
Step 4 — Group messages by conversation
Organize the collected messages into groups:
- For 1:1 DMs: group by the DM channel ID (each DM partner = one group)
- For group DMs: group by the group DM channel ID (each group conversation = one group)
- For channel @mentions: group by the channel ID
For each group, note:
- The channel name (from the search results)
- Whether it's a DM or channel mention
- The list of messages (sender name, timestamp, text, and permalink)
If there are no messages at all, skip to Step 6 — there's nothing to email. Write a brief note to the user like "No new DMs or @mentions found since [timestamp]."
Step 5 — Send emails
For each conversation group, compose and send an email using the Apps Script endpoint.
Email format:
- To: The
user_emailfrom config - Subject:
- For 1:1 DMs:
[Slack DM] New messages from {sender name} - For group DMs:
[Slack Group DM] New messages in group with {participant names} - For channel mentions:
[Slack] Mentioned in #{channel-name}
- For 1:1 DMs:
- Body: Use clean, readable HTML. Structure it like this:
<p>You have {N} new message(s) in this conversation.</p>
<div style="margin: 12px 0; padding: 10px; border-left: 3px solid #4A154B; background: #f9f9f9;">
<strong>{Sender Name}</strong> — <em>{formatted timestamp}</em><br>
{message text}
<br><a href="{permalink}">View in Slack</a>
</div>
<!-- repeat for each message in this group -->
Use the Slack purple (#4A154B) for the border to make it visually identifiable. Format timestamps in a human-friendly way (e.g., "Feb 27, 2026 at 3:30 PM").
Sending: Use the bundled scripts/send_email.py script. Do NOT use curl
for the POST — Google Apps Script's 302 redirect is incompatible with curl's
POST handling. Do NOT write your own Python code — use the script.
First, write a temporary JSON file containing an array of email actions. The to
field must use the user_email value from slack-to-email-config.json:
[
{
"to": "<user_email from config>",
"subject": "[Slack DM] New messages from Jane",
"body": "<p>HTML email body here</p>"
}
]
Then call the script:
python3 scripts/send_email.py "<apps_script_url>" "<apps_script_secret>" /tmp/emails.json
The script handles batching (10 emails per request), redirect handling, and error reporting. It prints a JSON result to stdout and exits with code 0 on success or 1 if any emails failed.
Why not curl? Google Apps Script responds with a 302 redirect. curl either
converts POST to GET (losing the payload) or, with --post302, sends POST to
a redirect target that only accepts GET (returning 405). The script uses
Python's urllib which handles the redirect correctly.
Check the response — if any email action has "status": "error", log the
error but continue with the remaining emails. Report failures to the user at
the end.
Do not improvise fallback delivery methods. If the Apps Script endpoint fails (returns an error, times out, or doesn't respond with valid JSON), report the failure to the user and stop. Do not attempt to send emails via Gmail drafts, Gmail API, or any other method — the Apps Script endpoint is the only supported delivery mechanism.
Step 6 — Update the run timestamp
Only if all emails sent successfully (or there were no messages to send),
write the current UTC time (the one you recorded in Step 2) to last-run.md
in the working directory, as a single line ISO 8601 timestamp.
If some emails failed, do NOT update the timestamp — this way the next run will pick up the messages that failed to send.
Report a summary to the user:
- How many conversations were found
- How many emails were sent
- Any errors encountered
Edge cases and notes
-
Slack user mentions in message text look like
<@U12345>. When composing the email body, replace these with the user's display name if possible (you can look up names withslack_read_user_profile). Don't go overboard — only resolve mentions that appear in the messages you're emailing. Cache lookups to avoid redundant API calls. -
Bot messages: The search might return bot messages. Include them — bots can @mention users with genuine requests (CI/CD alerts, Jira notifications, etc.).
-
Thread replies: If someone replies to a thread and @mentions the user, the search will return that reply. Include it as-is — the permalink will take the user to the right place in Slack.
-
Rate limiting: If you hit Slack search rate limits, wait a few seconds and retry. The Apps Script endpoint has its own limit of 10 emails per request, so respect that batch size.
-
Empty message text: Some Slack messages are attachments or file shares with no text body. For these, include a note like "[File shared]" or "[Attachment]" and still include the permalink so the user can view it in Slack.
-
Long messages: Slack messages can be very long. Include the full text in the email — don't truncate. The user wants to read the message without having to open Slack.
File reference
| File | Purpose |
|---|---|
references/apps-script-setup.md | Setup guide for the Apps Script deployment |
scripts/gmail-calendar-actions.gs | The Apps Script source code to deploy |
scripts/send_email.py | Sends emails via the Apps Script endpoint (handles redirects and batching) |
assets/slack-to-email-config-template.json | Config template (copy to working directory as slack-to-email-config.json) |
last-run.md | Tracks when the skill last ran (lives in user's working directory) |