allowFrom
This commit is contained in:
parent
22eb3d52a1
commit
f399bdfab1
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -69,8 +69,7 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
||||
"token",
|
||||
"emailAddress",
|
||||
"webhookPath",
|
||||
"allowlist",
|
||||
"blocklist",
|
||||
"allowFrom",
|
||||
],
|
||||
}),
|
||||
isConfigured: (account) => account.configured,
|
||||
@ -82,7 +81,7 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
||||
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<ResolvedAgentMailAccount> = {
|
||||
|
||||
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."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<AgentMailConfig, "allowlist" | "blocklist">,
|
||||
): FilterResult {
|
||||
const { allowlist = [], blocklist = [] } = config;
|
||||
config: Pick<AgentMailConfig, "allowFrom">,
|
||||
): 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<void> {
|
||||
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<AgentMailConfig, "allowlist" | "blocklist">,
|
||||
): Promise<FilterResult> {
|
||||
const result = checkSenderFilter(senderEmail, config);
|
||||
|
||||
if (result.label) {
|
||||
await labelMessage(client, inboxId, messageId, result.label);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user