From f399bdfab15ec3cea7b381ceb7dde8fd0d116ffb Mon Sep 17 00:00:00 2001 From: Haakam Aujla Date: Tue, 27 Jan 2026 13:06:24 -0800 Subject: [PATCH] allowFrom --- docs/channels/agentmail.md | 29 ++++----- extensions/agentmail/CHANGELOG.md | 2 +- extensions/agentmail/README.md | 2 +- extensions/agentmail/src/channel.ts | 21 ++++--- extensions/agentmail/src/config-schema.ts | 6 +- extensions/agentmail/src/filtering.test.ts | 48 ++++++--------- extensions/agentmail/src/filtering.ts | 65 +++++---------------- extensions/agentmail/src/monitor.ts | 22 +++---- extensions/agentmail/src/onboarding.test.ts | 14 +---- extensions/agentmail/src/onboarding.ts | 32 ++-------- extensions/agentmail/src/utils.ts | 7 --- 11 files changed, 77 insertions(+), 171 deletions(-) diff --git a/docs/channels/agentmail.md b/docs/channels/agentmail.md index e6d0d2d1c..160f3e63c 100644 --- a/docs/channels/agentmail.md +++ b/docs/channels/agentmail.md @@ -83,20 +83,18 @@ Minimal config: | `emailAddress` | string | AgentMail inbox email address to monitor (required) | | `webhookUrl` | string | Gateway public base URL (e.g., `https://gw.ngrok.io`) | | `webhookPath` | string | Custom webhook path (default: `/webhooks/agentmail`) | -| `allowlist` | string[] | Allowed sender emails/domains | -| `blocklist` | string[] | Blocked sender emails/domains | +| `allowFrom` | string[] | Allowed sender emails/domains (empty = allow all) | ## Sender Filtering -AgentMail supports allowlist and blocklist filtering for incoming emails. Both lists accept -email addresses and domains. +AgentMail uses `allowFrom` to filter incoming emails. The list accepts email addresses and domains. -### Allowlist/Blocklist Logic +### Filtering Logic -1. If sender is on the blocklist, the message is labeled `blocked` and ignored -2. If sender is on the allowlist, the message is labeled `allowed` and triggers Clawdbot -3. If allowlist is empty and sender is not blocked, the message is `allowed` (open mode) -4. If allowlist is non-empty and sender is not on it, the message is ignored +1. If `allowFrom` is empty, all senders are allowed (open mode) +2. If `allowFrom` is non-empty, only matching senders trigger Clawdbot +3. Allowed messages are labeled `allowed` in AgentMail +4. Non-matching senders are silently ignored ### Example Configuration @@ -108,9 +106,7 @@ email addresses and domains. token: "am_***", emailAddress: "clawd@agentmail.to", // Allow specific emails and domains - allowlist: ["alice@example.com", "trusted-domain.org"], - // Block specific emails and domains - blocklist: ["spam@bad.com", "bad-domain.net"], + allowFrom: ["alice@example.com", "trusted-domain.org"], }, }, } @@ -120,8 +116,7 @@ email addresses and domains. Domain entries match any email from that domain: -- `example.org` in allowlist allows `alice@example.org`, `bob@example.org`, etc. -- `bad.com` in blocklist blocks `spam@bad.com`, `anything@bad.com`, etc. +- `example.org` in allowFrom allows `alice@example.org`, `bob@example.org`, etc. ## Thread Context @@ -187,6 +182,6 @@ Then register the ngrok URL as your webhook endpoint in the AgentMail dashboard. ### Sender not allowed -1. Check the `allowlist` and `blocklist` configuration -2. Verify the sender email matches the expected format -3. Check if the sender's domain is blocked +1. Check the `allowFrom` configuration +2. Verify the sender email matches an entry in allowFrom +3. Remember: empty allowFrom means all senders are allowed diff --git a/extensions/agentmail/CHANGELOG.md b/extensions/agentmail/CHANGELOG.md index c8c1222c7..dd73f95fb 100644 --- a/extensions/agentmail/CHANGELOG.md +++ b/extensions/agentmail/CHANGELOG.md @@ -8,7 +8,7 @@ - Email channel integration via AgentMail API - Webhook-based inbound message handling - Full thread context fetching for conversation history -- Sender allowlist and blocklist filtering with automatic labeling +- Sender allowFrom filtering (consistent with other channels) - Attachment metadata in thread context - Agent tool for fetching attachment download URLs on demand - Interactive onboarding flow with inbox creation support diff --git a/extensions/agentmail/README.md b/extensions/agentmail/README.md index 2969fb6fd..ad24d8cdb 100644 --- a/extensions/agentmail/README.md +++ b/extensions/agentmail/README.md @@ -50,7 +50,7 @@ Register a webhook in the AgentMail dashboard: - Webhook-based inbound email handling - Full thread context for conversation history -- Sender allowlist/blocklist filtering +- Sender allowFrom filtering - Attachment metadata with on-demand download URLs - Interactive onboarding with inbox creation diff --git a/extensions/agentmail/src/channel.ts b/extensions/agentmail/src/channel.ts index 024f4d8d8..920a10844 100644 --- a/extensions/agentmail/src/channel.ts +++ b/extensions/agentmail/src/channel.ts @@ -69,8 +69,7 @@ export const agentmailPlugin: ChannelPlugin = { "token", "emailAddress", "webhookPath", - "allowlist", - "blocklist", + "allowFrom", ], }), isConfigured: (account) => account.configured, @@ -82,7 +81,7 @@ export const agentmailPlugin: ChannelPlugin = { emailAddress: account.inboxId, }), resolveAllowFrom: ({ cfg }) => - ((cfg as CoreConfig).channels?.agentmail?.allowlist ?? []).map((entry) => + ((cfg as CoreConfig).channels?.agentmail?.allowFrom ?? []).map((entry) => String(entry) ), formatAllowFrom: ({ allowFrom }) => @@ -93,21 +92,21 @@ export const agentmailPlugin: ChannelPlugin = { security: { resolveDmPolicy: ({ account }) => ({ - policy: "open", // AgentMail uses allowlist/blocklist instead of pairing - allowFrom: account.config.allowlist ?? [], - policyPath: "channels.agentmail.allowlist", - allowFromPath: "channels.agentmail.allowlist", + policy: "open", + allowFrom: account.config.allowFrom ?? [], + policyPath: "channels.agentmail.allowFrom", + allowFromPath: "channels.agentmail.allowFrom", approveHint: - "Add email addresses or domains to channels.agentmail.allowlist", + "Add email addresses or domains to channels.agentmail.allowFrom", normalizeEntry: (raw) => raw.toLowerCase().trim(), }), collectWarnings: ({ account }) => { const warnings: string[] = []; - const { allowlist = [], blocklist = [] } = account.config; + const { allowFrom = [] } = account.config; - if (allowlist.length === 0 && blocklist.length === 0) { + if (allowFrom.length === 0) { warnings.push( - "- AgentMail: No allowlist or blocklist configured. All senders will be allowed." + "- AgentMail: No allowFrom configured. All senders will be allowed." ); } diff --git a/extensions/agentmail/src/config-schema.ts b/extensions/agentmail/src/config-schema.ts index 3567826de..73d605046 100644 --- a/extensions/agentmail/src/config-schema.ts +++ b/extensions/agentmail/src/config-schema.ts @@ -17,8 +17,6 @@ export const AgentMailConfigSchema = z.object({ webhookUrl: z.string().optional(), /** Custom webhook path for receiving emails (default: /webhooks/agentmail). */ webhookPath: z.string().optional(), - /** Allowlist of email addresses and/or domains. */ - allowlist: z.array(z.string()).optional(), - /** Blocklist of email addresses and/or domains. */ - blocklist: z.array(z.string()).optional(), + /** Allowed sender emails/domains. Empty = allow all. */ + allowFrom: z.array(z.string()).optional(), }); diff --git a/extensions/agentmail/src/filtering.test.ts b/extensions/agentmail/src/filtering.test.ts index 27e741226..c5d6a41bf 100644 --- a/extensions/agentmail/src/filtering.test.ts +++ b/extensions/agentmail/src/filtering.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { checkSenderFilter, matchesList } from "./filtering.js"; +import { checkSenderAllowed, matchesList } from "./filtering.js"; describe("matchesList", () => { it("returns false for empty list", () => { @@ -32,44 +32,32 @@ describe("matchesList", () => { }); }); -describe("checkSenderFilter", () => { - it("blocks sender on blocklist", () => { - const result = checkSenderFilter("spam@bad.com", { - blocklist: ["bad.com"], - allowlist: [], +describe("checkSenderAllowed", () => { + it("allows sender on allowFrom", () => { + const result = checkSenderAllowed("friend@good.com", { + allowFrom: ["good.com"], }); - expect(result).toEqual({ allowed: false, blocked: true, label: "blocked" }); + expect(result).toBe(true); }); - it("allows sender on allowlist", () => { - const result = checkSenderFilter("friend@good.com", { - blocklist: [], - allowlist: ["good.com"], + it("allows all in open mode (empty allowFrom)", () => { + const result = checkSenderAllowed("anyone@anywhere.com", { + allowFrom: [], }); - expect(result).toEqual({ allowed: true, blocked: false, label: "allowed" }); + expect(result).toBe(true); }); - it("allows all non-blocked in open mode (empty allowlist)", () => { - const result = checkSenderFilter("anyone@anywhere.com", { - blocklist: [], - allowlist: [], + it("rejects sender not on non-empty allowFrom", () => { + const result = checkSenderAllowed("stranger@unknown.com", { + allowFrom: ["trusted.com"], }); - expect(result).toEqual({ allowed: true, blocked: false, label: "allowed" }); + expect(result).toBe(false); }); - it("rejects sender not on non-empty allowlist", () => { - const result = checkSenderFilter("stranger@unknown.com", { - blocklist: [], - allowlist: ["trusted.com"], + it("matches exact email in allowFrom", () => { + const result = checkSenderAllowed("user@example.com", { + allowFrom: ["user@example.com"], }); - expect(result).toEqual({ allowed: false, blocked: false, label: null }); - }); - - it("blocklist takes precedence over allowlist", () => { - const result = checkSenderFilter("user@both.com", { - blocklist: ["both.com"], - allowlist: ["both.com"], - }); - expect(result).toEqual({ allowed: false, blocked: true, label: "blocked" }); + expect(result).toBe(true); }); }); diff --git a/extensions/agentmail/src/filtering.ts b/extensions/agentmail/src/filtering.ts index 01b515e20..b7119b56c 100644 --- a/extensions/agentmail/src/filtering.ts +++ b/extensions/agentmail/src/filtering.ts @@ -1,6 +1,6 @@ import type { AgentMailClient } from "agentmail"; -import type { AgentMailConfig, FilterResult } from "./utils.js"; +import type { AgentMailConfig } from "./utils.js"; /** * Checks if a sender email matches any entry in a list. @@ -23,69 +23,36 @@ export function matchesList(senderEmail: string, list: string[]): boolean { } /** - * Determines the filter result for a sender based on allowlist/blocklist. + * Determines if a sender is allowed based on allowFrom config. * * Logic: - * 1. If sender is on blocklist → blocked - * 2. If sender is on allowlist → allowed - * 3. If allowlist is empty and not blocked → allowed (open mode) - * 4. If allowlist is non-empty and sender not on it → not allowed + * 1. If allowFrom is empty → allowed (open mode) + * 2. If sender matches allowFrom → allowed + * 3. Otherwise → not allowed */ -export function checkSenderFilter( +export function checkSenderAllowed( senderEmail: string, - config: Pick, -): FilterResult { - const { allowlist = [], blocklist = [] } = config; + config: Pick, +): boolean { + const { allowFrom = [] } = config; - // Check blocklist first - if (matchesList(senderEmail, blocklist)) { - return { allowed: false, blocked: true, label: "blocked" }; + // Open mode: all senders allowed when allowFrom is empty + if (allowFrom.length === 0) { + return true; } - // Check allowlist - if (allowlist.length === 0) { - // Open mode: all non-blocked senders are allowed - return { allowed: true, blocked: false, label: "allowed" }; - } - - if (matchesList(senderEmail, allowlist)) { - return { allowed: true, blocked: false, label: "allowed" }; - } - - // Not on allowlist and allowlist is non-empty - return { allowed: false, blocked: false, label: null }; + return matchesList(senderEmail, allowFrom); } /** - * Labels a message via the AgentMail API. + * Labels a message as "allowed" via the AgentMail API. */ -export async function labelMessage( +export async function labelMessageAllowed( client: AgentMailClient, inboxId: string, messageId: string, - label: "allowed" | "blocked", ): Promise { await client.inboxes.messages.update(inboxId, messageId, { - addLabels: [label], + addLabels: ["allowed"], }); } - -/** - * Processes an incoming message and applies filtering. - * Returns the filter result and labels the message if needed. - */ -export async function filterAndLabelMessage( - client: AgentMailClient, - inboxId: string, - messageId: string, - senderEmail: string, - config: Pick, -): Promise { - const result = checkSenderFilter(senderEmail, config); - - if (result.label) { - await labelMessage(client, inboxId, messageId, result.label); - } - - return result; -} diff --git a/extensions/agentmail/src/monitor.ts b/extensions/agentmail/src/monitor.ts index 8ada3260a..dc119a311 100644 --- a/extensions/agentmail/src/monitor.ts +++ b/extensions/agentmail/src/monitor.ts @@ -5,7 +5,7 @@ import { registerPluginHttpRoute } from "clawdbot/plugin-sdk"; import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js"; import { getAgentMailClient } from "./client.js"; import { getAgentMailRuntime } from "./runtime.js"; -import { filterAndLabelMessage } from "./filtering.js"; +import { checkSenderAllowed, labelMessageAllowed } from "./filtering.js"; import { extractMessageBody, fetchFormattedThread } from "./thread.js"; import { parseEmailFromAddress, parseNameFromAddress } from "./utils.js"; import type { CoreConfig, MessageReceivedEvent } from "./utils.js"; @@ -111,8 +111,7 @@ export async function monitorAgentMailProvider( } const client = getAgentMailClient(apiKey); - const allowlist = agentmailConfig?.allowlist ?? []; - const blocklist = agentmailConfig?.blocklist ?? []; + const allowFrom = agentmailConfig?.allowFrom ?? []; recordState(accountId, { running: true, @@ -152,21 +151,16 @@ export async function monitorAgentMailProvider( logVerbose(`agentmail: received message from ${senderEmail}`); // Apply sender filtering - const filterResult = await filterAndLabelMessage( - client, - inboxId, - message.messageId, - senderEmail, - { allowlist, blocklist } - ); + const allowed = checkSenderAllowed(senderEmail, { allowFrom }); - if (!filterResult.allowed) { - logVerbose( - `agentmail: sender ${senderEmail} not allowed (blocked=${filterResult.blocked})` - ); + if (!allowed) { + logVerbose(`agentmail: sender ${senderEmail} not in allowFrom`); return sendJson(res, 200, { ok: true, filtered: true }); } + // Label message as allowed + await labelMessageAllowed(client, inboxId, message.messageId); + recordState(accountId, { lastInboundAt: Date.now() }); // Fetch the full thread from API (handles webhook size limits for large messages) diff --git a/extensions/agentmail/src/onboarding.test.ts b/extensions/agentmail/src/onboarding.test.ts index d93faf555..08ecd2641 100644 --- a/extensions/agentmail/src/onboarding.test.ts +++ b/extensions/agentmail/src/onboarding.test.ts @@ -77,25 +77,17 @@ describe("updateAgentMailConfig", () => { expect(result.channels?.agentmail?.enabled).toBe(true); }); - it("adds allowlist", () => { + it("adds allowFrom", () => { const cfg = {} as never; const result: AnyConfig = updateAgentMailConfig(cfg, { - allowlist: ["user@example.com", "example.org"], + allowFrom: ["user@example.com", "example.org"], }); - expect(result.channels?.agentmail?.allowlist).toEqual([ + expect(result.channels?.agentmail?.allowFrom).toEqual([ "user@example.com", "example.org", ]); }); - it("adds blocklist", () => { - const cfg = {} as never; - const result: AnyConfig = updateAgentMailConfig(cfg, { - blocklist: ["spam@bad.com"], - }); - expect(result.channels?.agentmail?.blocklist).toEqual(["spam@bad.com"]); - }); - it("sets webhookPath", () => { const cfg = {} as never; const result: AnyConfig = updateAgentMailConfig(cfg, { diff --git a/extensions/agentmail/src/onboarding.ts b/extensions/agentmail/src/onboarding.ts index 64eb7dace..abc20b140 100644 --- a/extensions/agentmail/src/onboarding.ts +++ b/extensions/agentmail/src/onboarding.ts @@ -263,13 +263,13 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { // Save webhook config next = updateAgentMailConfig(next, { webhookUrl, webhookPath }); - // Ask about allowlist - const addAllowlist = await prompter.confirm({ - message: "Add senders to allowlist? (Empty = allow all non-blocked)", + // Ask about allowFrom + const addAllowFrom = await prompter.confirm({ + message: "Add senders to allowFrom? (Empty = allow all)", initialValue: false, }); - if (addAllowlist) { + if (addAllowFrom) { const entry = String( await prompter.text({ message: @@ -279,28 +279,8 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { if (entry) { const existing = - (next as CoreConfig).channels?.agentmail?.allowlist ?? []; - next = updateAgentMailConfig(next, { allowlist: [...existing, entry] }); - } - } - - // Ask about blocklist - const addBlocklist = await prompter.confirm({ - message: "Add senders to blocklist? (Block spam/unwanted emails)", - initialValue: false, - }); - - if (addBlocklist) { - const entry = String( - await prompter.text({ - message: "Email or domain to block (e.g., spam@bad.com or bad.com)", - }) - ).trim(); - - if (entry) { - const existing = - (next as CoreConfig).channels?.agentmail?.blocklist ?? []; - next = updateAgentMailConfig(next, { blocklist: [...existing, entry] }); + (next as CoreConfig).channels?.agentmail?.allowFrom ?? []; + next = updateAgentMailConfig(next, { allowFrom: [...existing, entry] }); } } diff --git a/extensions/agentmail/src/utils.ts b/extensions/agentmail/src/utils.ts index 8c4e744d1..f2b7d2f67 100644 --- a/extensions/agentmail/src/utils.ts +++ b/extensions/agentmail/src/utils.ts @@ -34,13 +34,6 @@ export type Message = AgentMail.messages.Message; /** Re-export SDK webhook event type. */ export type MessageReceivedEvent = AgentMail.events.MessageReceivedEvent; -/** Result of sender filtering check. */ -export type FilterResult = { - allowed: boolean; - blocked: boolean; - label: "allowed" | "blocked" | null; -}; - /** Formats a date as a UTC string. */ export function formatUtcDate(date: Date | string | number): string { return new Date(date).toUTCString().replace("GMT", "UTC");