Turn slash commands and workflow automations into governed email actions. MultiMail's approval queue means a chat message never becomes an unreviewed outbound email.
Slack is where agents and humans already collaborate — so it's a natural place to trigger email actions. A sales bot summarizes an inbox thread on demand. A support workflow routes inbound emails to the right channel. A scheduled bot drafts and queues outbound messages for human review. These patterns all work, but they require a layer between the chat command and the SMTP relay.
MultiMail sits at that layer. When a Slack bot calls `send_email`, the request enters MultiMail's oversight pipeline rather than going directly to delivery. Depending on the oversight mode you configure per mailbox, the email is either queued for approval, sent with a notification, or delivered autonomously. The Slack user who typed the command sees an immediate confirmation; the email policy is enforced regardless of how casual the command felt.
The Bolt SDK makes it straightforward to wire up slash commands and workflow steps to MultiMail's REST API. You get typed request/response shapes, webhook delivery for approval events, and an audit trail that satisfies compliance requirements — without building any of that infrastructure yourself.
Any email triggered by a slash command or workflow step enters MultiMail's pending queue when the mailbox is in `gated_send` mode. Approvers see the full message before delivery. A casual `/send-proposal` command cannot bypass review.
Set `gated_all` on executive mailboxes, `monitored` on internal digest bots, and `autonomous` on transactional notification senders — all within the same Slack app. Oversight is a property of the mailbox, not the bot.
MultiMail fires `message.approved`, `message.rejected`, and `message.delivered` webhooks that your Bolt app can relay back to the originating channel. The agent loop closes: Slack → MultiMail → approved → Slack notification.
Subscribe a Slack channel to inbound webhook events on a MultiMail mailbox. Replies, tag changes, and thread updates stream to Slack in real time, making email activity visible without building a polling loop.
MultiMail enforces that outbound email matches the authenticated mailbox identity. A Slack bot cannot spoof a From address it does not own, even if the slash command attempts to override it.
Every action — send, approve, reject, tag, decide — is recorded with the originating API key, timestamp, and message ID. CAN-SPAM and GDPR audit requests are answerable without scraping Slack history.
import { App, AwsLambdaReceiver } from '@slack/bolt';
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
receiver: new AwsLambdaReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET }),
});
"cm">// Step 1: open a modal to collect email fields
app.command('/send-email', async ({ ack, client, body }) => {
await ack();
await client.views.open({
trigger_id: body.trigger_id,
view: {
type: 'modal',
callback_id: 'compose_email',
title: { type: 'plain_text', text: 'Send Email' },
submit: { type: 'plain_text', text: 'Send' },
blocks: [
{ type: 'input', block_id: 'to', label: { type: 'plain_text', text: 'To' },
element: { type: 'plain_text_input', action_id: 'to_input' } },
{ type: 'input', block_id: 'subject', label: { type: 'plain_text', text: 'Subject' },
element: { type: 'plain_text_input', action_id: 'subject_input' } },
{ type: 'input', block_id: 'body', label: { type: 'plain_text', text: 'Message' },
element: { type: 'plain_text_input', action_id: 'body_input', multiline: true } },
],
},
});
});
"cm">// Step 2: submit modal → call MultiMail
app.view('compose_email', async ({ ack, view, client, body }) => {
await ack();
const vals = view.state.values;
const to = vals.to.to_input.value;
const subject = vals.subject.subject_input.value;
const text = vals.body.body_input.value;
const res = await fetch('https://api.multimail.dev/v1/email/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: '[email protected]',
to,
subject,
text,
}),
});
const result = await res.json();
const isQueued = result.status === 'pending_approval';
await client.chat.postMessage({
channel: body.user.id,
text: isQueued
? `Email queued for approval. Message ID: \`${result.message_id}\`. A reviewer will approve or reject it before delivery.`
: `Email sent. Message ID: \`${result.message_id}\``,
});
});A `/send-email` slash command collects recipient, subject, and body from a Slack modal, then calls MultiMail. With `gated_send` oversight, the API returns a pending message ID rather than delivering immediately.
import express from 'express';
import { App, ExpressReceiver } from '@slack/bolt';
const receiver = new ExpressReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({ token: process.env.SLACK_BOT_TOKEN, receiver });
"cm">// MultiMail posts inbound email events to this route
receiver.router.post('/multimail/inbound', express.json(), async (req, res) => {
"cm">// Verify MultiMail webhook signature (compare X-MultiMail-Signature header)
const sig = req.headers['x-multimail-signature'];
if (sig !== process.env.MULTIMAIL_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'invalid signature' });
}
const { from, subject, text, message_id } = req.body;
await app.client.chat.postMessage({
channel: process.env.EMAIL_ALERTS_CHANNEL_ID,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*New email* in agent inbox\n*From:* ${from}\n*Subject:* ${subject}`,
},
},
{
type: 'section',
text: { type: 'plain_text', text: text.slice(0, 300) },
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'Read full email' },
action_id: 'read_email',
value: message_id,
},
{
type: 'button',
text: { type: 'plain_text', text: 'Reply' },
action_id: 'reply_email',
value: message_id,
style: 'primary',
},
],
},
],
});
res.json({ ok: true });
});
"cm">// Handle read full email button
app.action('read_email', async ({ ack, body, client, action }) => {
await ack();
const message_id = action.value;
const res = await fetch(`https:"cm">//api.multimail.dev/v1/email/${message_id}`, {
headers: { Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}` },
});
const email = await res.json();
await client.views.open({
trigger_id: body.trigger_id,
view: {
type: 'modal',
title: { type: 'plain_text', text: email.subject },
blocks: [
{ type: 'section', text: { type: 'plain_text', text: email.text } },
],
},
});
});
(async () => { await app.start(3000); })();Register a MultiMail inbound webhook and relay new emails to a Slack channel. Uses Bolt's HTTP receiver and formats a clean notification block.
import express from 'express';
import { App, ExpressReceiver } from '@slack/bolt';
const receiver = new ExpressReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({ token: process.env.SLACK_BOT_TOKEN, receiver });
"cm">// Store message_id → slack_user_id during send (in KV or Redis)
const pendingMap = new Map();
receiver.router.post('/multimail/approval', express.json(), async (req, res) => {
const sig = req.headers['x-multimail-signature'];
if (sig !== process.env.MULTIMAIL_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'invalid signature' });
}
const { event, message_id, subject, to, approved_by } = req.body;
"cm">// event is 'message.approved' or 'message.rejected'
const slackUserId = pendingMap.get(message_id);
if (!slackUserId) return res.json({ ok: true }); "cm">// unknown origin
if (event === 'message.approved') {
await app.client.chat.postMessage({
channel: slackUserId,
text: `:white_check_mark: Email approved and sent.\n*To:* ${to}\n*Subject:* ${subject}\n*Approved by:* ${approved_by}`,
});
} else if (event === 'message.rejected') {
await app.client.chat.postMessage({
channel: slackUserId,
text: `:x: Email rejected and cancelled.\n*To:* ${to}\n*Subject:* ${subject}\n*Rejected by:* ${approved_by}`,
});
}
pendingMap.delete(message_id);
res.json({ ok: true });
});
"cm">// Usage: after calling /v1/email/send and getting status=pending_approval:
"cm">// pendingMap.set(result.message_id, slackUserId);
(async () => { await app.start(3000); })();When a reviewer approves or rejects a pending email in MultiMail, post the outcome back to the Slack user who triggered the send. Closes the agent feedback loop without polling.
import { App } from '@slack/bolt';
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
app.command('/inbox-summary', async ({ ack, respond }) => {
await ack();
const res = await fetch(
'https:"cm">//api.multimail.dev/v1/[email protected]&unread=true&limit=5',
{ headers: { Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}` } }
);
const { emails, total_unread } = await res.json();
if (!emails.length) {
return respond({ text: 'No unread emails in the agent mailbox.' });
}
const blocks = [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Inbox summary* — ${total_unread} unread email${total_unread !== 1 ? 's' : ''}`,
},
},
{ type: 'divider' },
...emails.map((email) => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `*${email.subject}*\nFrom: ${email.from}\n${email.snippet}`,
},
accessory: {
type: 'button',
text: { type: 'plain_text', text: 'Read' },
action_id: 'read_email',
value: email.message_id,
},
})),
];
await respond({ blocks });
});
(async () => { await app.start(3000); })();Pull recent unread emails from a MultiMail mailbox and post a digest to the channel. Useful for agent briefing workflows triggered by a morning standup command.
Create a new Slack app at api.slack.com/apps with `commands`, `chat:write`, and `views:open` OAuth scopes. Install the Bolt SDK.
npm install @slack/bolt
"cm"># Set environment variables
export SLACK_BOT_TOKEN=xoxb-...
export SLACK_SIGNING_SECRET=...Log into MultiMail and create a mailbox for your Slack agent (e.g., `[email protected]`). Set oversight mode to `gated_send` so slash-command-triggered emails require approval. Generate an API key with `email:send` and `inbox:read` scopes.
curl -X POST https://api.multimail.dev/v1/mailboxes \
-H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
-H &"cm">#039;Content-Type: application/json' \
-d &"cm">#039;{"address": "[email protected]", "oversight_mode": "gated_send"}'Register your Bolt app's public endpoint to receive inbound email events and approval notifications from MultiMail.
curl -X POST https://api.multimail.dev/v1/webhooks \
-H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
-H &"cm">#039;Content-Type: application/json' \
-d &"cm">#039;{
"url": "https://your-bolt-app.acme.com/multimail/inbound",
"events": ["email.received", "message.approved", "message.rejected"],
"mailbox": "[email protected]",
"secret": "your-webhook-signing-secret"
}&"cm">#039;Add a `/send-email` command in your Slack app settings pointing to your Bolt server. Implement the handler to open a modal and submit to MultiMail on confirm. See the code sample above.
"cm"># In Slack App settings:
"cm"># Slash Commands → /send-email → Request URL: https://your-bolt-app.acme.com/slack/events
export MULTIMAIL_API_KEY=mm_live_...Use a MultiMail test API key (`mm_test_...`) to trigger the slash command without sending real email. Verify that the pending message appears in the MultiMail dashboard and that your approval webhook fires correctly.
"cm"># Send a test email via API with test key
curl -X POST https://api.multimail.dev/v1/email/send \
-H &"cm">#039;Authorization: Bearer mm_test_...' \
-H &"cm">#039;Content-Type: application/json' \
-d &"cm">#039;{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Test from Slack agent",
"text": "This is a test message from the Slack integration."
}&"cm">#039;
"cm"># Check pending messages
curl https://api.multimail.dev/v1/messages/pending \
-H &"cm">#039;Authorization: Bearer mm_test_...'Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.