Consolidate
This commit is contained in:
parent
4dbd7ab0ce
commit
258a4bbe49
@ -1,11 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js";
|
||||||
listAgentMailAccountIds,
|
|
||||||
resolveAgentMailAccount,
|
|
||||||
resolveCredentials,
|
|
||||||
resolveDefaultAgentMailAccountId,
|
|
||||||
} from "./accounts.js";
|
|
||||||
import type { CoreConfig } from "./utils.js";
|
import type { CoreConfig } from "./utils.js";
|
||||||
|
|
||||||
describe("resolveCredentials", () => {
|
describe("resolveCredentials", () => {
|
||||||
@ -99,7 +94,9 @@ describe("resolveCredentials", () => {
|
|||||||
AGENTMAIL_WEBHOOK_URL: "https://env-gateway.example.com/hooks/email",
|
AGENTMAIL_WEBHOOK_URL: "https://env-gateway.example.com/hooks/email",
|
||||||
};
|
};
|
||||||
const result = resolveCredentials(cfg, env);
|
const result = resolveCredentials(cfg, env);
|
||||||
expect(result.webhookUrl).toBe("https://env-gateway.example.com/hooks/email");
|
expect(result.webhookUrl).toBe(
|
||||||
|
"https://env-gateway.example.com/hooks/email"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("derives webhookPath from webhookUrl", () => {
|
it("derives webhookPath from webhookUrl", () => {
|
||||||
@ -218,19 +215,3 @@ describe("resolveAgentMailAccount", () => {
|
|||||||
expect(result.name).toBe("Trimmed Name");
|
expect(result.name).toBe("Trimmed Name");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("listAgentMailAccountIds", () => {
|
|
||||||
it("returns default account", () => {
|
|
||||||
const cfg: CoreConfig = {};
|
|
||||||
const result = listAgentMailAccountIds(cfg);
|
|
||||||
expect(result).toEqual(["default"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("resolveDefaultAgentMailAccountId", () => {
|
|
||||||
it("returns default account id", () => {
|
|
||||||
const cfg: CoreConfig = {};
|
|
||||||
const result = resolveDefaultAgentMailAccountId(cfg);
|
|
||||||
expect(result).toBe("default");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@ -6,22 +6,6 @@ import type {
|
|||||||
ResolvedAgentMailAccount,
|
ResolvedAgentMailAccount,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists all AgentMail account IDs.
|
|
||||||
* Currently supports only a single default account.
|
|
||||||
*/
|
|
||||||
export function listAgentMailAccountIds(_cfg: CoreConfig): string[] {
|
|
||||||
return [DEFAULT_ACCOUNT_ID];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the default AgentMail account ID.
|
|
||||||
* Currently only supports a single default account.
|
|
||||||
*/
|
|
||||||
export function resolveDefaultAgentMailAccountId(_cfg: CoreConfig): string {
|
|
||||||
return DEFAULT_ACCOUNT_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolved AgentMail credentials and paths. */
|
/** Resolved AgentMail credentials and paths. */
|
||||||
export type ResolvedAgentMailCredentials = {
|
export type ResolvedAgentMailCredentials = {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@ -30,10 +14,10 @@ export type ResolvedAgentMailCredentials = {
|
|||||||
webhookPath: string;
|
webhookPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_WEBHOOK_PATH = "/webhooks/agentmail";
|
export const DEFAULT_WEBHOOK_PATH = "/webhooks/agentmail";
|
||||||
|
|
||||||
/** Extracts the path from a URL string. Returns undefined if just root "/". */
|
/** Extracts the path from a URL string. Returns undefined if just root "/". */
|
||||||
function extractPathFromUrl(url: string): string | undefined {
|
export function extractPathFromUrl(url: string): string | undefined {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
// Return undefined if just root path "/" - use default instead
|
// Return undefined if just root path "/" - use default instead
|
||||||
|
|||||||
@ -8,12 +8,7 @@ import {
|
|||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import {
|
import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js";
|
||||||
listAgentMailAccountIds,
|
|
||||||
resolveAgentMailAccount,
|
|
||||||
resolveDefaultAgentMailAccountId,
|
|
||||||
resolveCredentials,
|
|
||||||
} from "./accounts.js";
|
|
||||||
import { getAgentMailClient } from "./client.js";
|
import { getAgentMailClient } from "./client.js";
|
||||||
import { AgentMailConfigSchema } from "./config-schema.js";
|
import { AgentMailConfigSchema } from "./config-schema.js";
|
||||||
import { agentmailOnboardingAdapter } from "./onboarding.js";
|
import { agentmailOnboardingAdapter } from "./onboarding.js";
|
||||||
@ -47,11 +42,10 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
|||||||
configSchema: buildChannelConfigSchema(AgentMailConfigSchema),
|
configSchema: buildChannelConfigSchema(AgentMailConfigSchema),
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
listAccountIds: (cfg) => listAgentMailAccountIds(cfg as CoreConfig),
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
||||||
resolveAccount: (cfg, accountId) =>
|
resolveAccount: (cfg, accountId) =>
|
||||||
resolveAgentMailAccount({ cfg: cfg as CoreConfig, accountId }),
|
resolveAgentMailAccount({ cfg: cfg as CoreConfig, accountId }),
|
||||||
defaultAccountId: (cfg) =>
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||||
resolveDefaultAgentMailAccountId(cfg as CoreConfig),
|
|
||||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||||
setAccountEnabledInConfigSection({
|
setAccountEnabledInConfigSection({
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as CoreConfig,
|
||||||
@ -102,18 +96,10 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
|||||||
"Add email addresses or domains to channels.agentmail.allowFrom",
|
"Add email addresses or domains to channels.agentmail.allowFrom",
|
||||||
normalizeEntry: (raw) => raw.toLowerCase().trim(),
|
normalizeEntry: (raw) => raw.toLowerCase().trim(),
|
||||||
}),
|
}),
|
||||||
collectWarnings: ({ account }) => {
|
collectWarnings: ({ account }) =>
|
||||||
const warnings: string[] = [];
|
(account.config.allowFrom?.length ?? 0) === 0
|
||||||
const { allowFrom = [] } = account.config;
|
? ["- AgentMail: No allowFrom configured. All senders will be allowed."]
|
||||||
|
: [],
|
||||||
if (allowFrom.length === 0) {
|
|
||||||
warnings.push(
|
|
||||||
"- AgentMail: No allowFrom configured. All senders will be allowed."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return warnings;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
messaging: {
|
messaging: {
|
||||||
@ -147,25 +133,19 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
|||||||
},
|
},
|
||||||
applyAccountConfig: ({ cfg, input }) => {
|
applyAccountConfig: ({ cfg, input }) => {
|
||||||
const existing = (cfg as CoreConfig).channels?.agentmail ?? {};
|
const existing = (cfg as CoreConfig).channels?.agentmail ?? {};
|
||||||
|
const updates: Record<string, unknown> = { enabled: true };
|
||||||
|
if (!input.useEnv) {
|
||||||
|
if (input.token?.trim()) updates.token = input.token.trim();
|
||||||
|
if (input.emailAddress?.trim())
|
||||||
|
updates.emailAddress = input.emailAddress.trim();
|
||||||
|
if (input.webhookPath?.trim())
|
||||||
|
updates.webhookPath = input.webhookPath.trim();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...cfg,
|
...cfg,
|
||||||
channels: {
|
channels: {
|
||||||
...(cfg as CoreConfig).channels,
|
...(cfg as CoreConfig).channels,
|
||||||
agentmail: {
|
agentmail: { ...existing, ...updates },
|
||||||
...existing,
|
|
||||||
enabled: true,
|
|
||||||
...(input.useEnv
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
...(input.token?.trim() ? { token: input.token.trim() } : {}),
|
|
||||||
...(input.emailAddress?.trim()
|
|
||||||
? { emailAddress: input.emailAddress.trim() }
|
|
||||||
: {}),
|
|
||||||
...(input.webhookPath?.trim()
|
|
||||||
? { webhookPath: input.webhookPath.trim() }
|
|
||||||
: {}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -182,54 +162,39 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
|||||||
lastError: null,
|
lastError: null,
|
||||||
},
|
},
|
||||||
collectStatusIssues: (accounts) =>
|
collectStatusIssues: (accounts) =>
|
||||||
accounts.flatMap((account) => {
|
accounts
|
||||||
const lastError =
|
.filter((a) => typeof a.lastError === "string" && a.lastError.trim())
|
||||||
typeof account.lastError === "string" ? account.lastError.trim() : "";
|
.map((a) => ({
|
||||||
if (!lastError) return [];
|
channel: "agentmail",
|
||||||
return [
|
accountId: a.accountId,
|
||||||
{
|
kind: "runtime" as const,
|
||||||
channel: "agentmail",
|
message: `Channel error: ${a.lastError}`,
|
||||||
accountId: account.accountId,
|
})),
|
||||||
kind: "runtime",
|
buildChannelSummary: ({ snapshot: s }) => ({
|
||||||
message: `Channel error: ${lastError}`,
|
configured: s.configured ?? false,
|
||||||
},
|
emailAddress: s.emailAddress ?? null,
|
||||||
];
|
running: s.running ?? false,
|
||||||
}),
|
lastStartAt: s.lastStartAt ?? null,
|
||||||
buildChannelSummary: ({ snapshot }) => ({
|
lastStopAt: s.lastStopAt ?? null,
|
||||||
configured: snapshot.configured ?? false,
|
lastError: s.lastError ?? null,
|
||||||
emailAddress: snapshot.emailAddress ?? null,
|
probe: s.probe,
|
||||||
running: snapshot.running ?? false,
|
lastProbeAt: s.lastProbeAt ?? null,
|
||||||
lastStartAt: snapshot.lastStartAt ?? null,
|
|
||||||
lastStopAt: snapshot.lastStopAt ?? null,
|
|
||||||
lastError: snapshot.lastError ?? null,
|
|
||||||
probe: snapshot.probe,
|
|
||||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
||||||
}),
|
}),
|
||||||
probeAccount: async ({ cfg }) => {
|
probeAccount: async ({ cfg }) => {
|
||||||
try {
|
try {
|
||||||
const { apiKey, inboxId } = resolveCredentials(cfg as CoreConfig);
|
const { apiKey, inboxId } = resolveCredentials(cfg as CoreConfig);
|
||||||
|
if (!apiKey || !inboxId)
|
||||||
if (!apiKey || !inboxId) {
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "Missing token or email address",
|
error: "Missing token or email address",
|
||||||
elapsedMs: 0,
|
elapsedMs: 0,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const client = getAgentMailClient(apiKey);
|
const inbox = await getAgentMailClient(apiKey).inboxes.get(inboxId);
|
||||||
|
|
||||||
// Probe by getting inbox info
|
|
||||||
const inbox = await client.inboxes.get(inboxId);
|
|
||||||
const elapsedMs = Date.now() - start;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
elapsedMs,
|
elapsedMs: Date.now() - start,
|
||||||
meta: {
|
meta: { inboxId: inbox.inboxId },
|
||||||
inboxId: inbox.inboxId, // inboxId is the email address
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
@ -239,40 +204,35 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
buildAccountSnapshot: ({ account: a, runtime: r, probe }) => ({
|
||||||
accountId: account.accountId,
|
accountId: a.accountId,
|
||||||
name: account.name,
|
name: a.name,
|
||||||
enabled: account.enabled,
|
enabled: a.enabled,
|
||||||
configured: account.configured,
|
configured: a.configured,
|
||||||
emailAddress: account.inboxId,
|
emailAddress: a.inboxId,
|
||||||
running: runtime?.running ?? false,
|
running: r?.running ?? false,
|
||||||
lastStartAt: runtime?.lastStartAt ?? null,
|
lastStartAt: r?.lastStartAt ?? null,
|
||||||
lastStopAt: runtime?.lastStopAt ?? null,
|
lastStopAt: r?.lastStopAt ?? null,
|
||||||
lastError: runtime?.lastError ?? null,
|
lastError: r?.lastError ?? null,
|
||||||
probe,
|
probe,
|
||||||
lastProbeAt: runtime?.lastProbeAt ?? null,
|
lastProbeAt: r?.lastProbeAt ?? null,
|
||||||
lastInboundAt: runtime?.lastInboundAt ?? null,
|
lastInboundAt: r?.lastInboundAt ?? null,
|
||||||
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
lastOutboundAt: r?.lastOutboundAt ?? null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
gateway: {
|
gateway: {
|
||||||
startAccount: async (ctx) => {
|
startAccount: async (ctx) => {
|
||||||
const account = ctx.account;
|
const { accountId, inboxId } = ctx.account;
|
||||||
ctx.setStatus({
|
ctx.setStatus({ accountId, emailAddress: inboxId });
|
||||||
accountId: account.accountId,
|
|
||||||
emailAddress: account.inboxId,
|
|
||||||
});
|
|
||||||
ctx.log?.info(
|
ctx.log?.info(
|
||||||
`[${account.accountId}] starting AgentMail provider (email: ${
|
`[${accountId}] starting AgentMail provider (email: ${
|
||||||
account.inboxId ?? "unknown"
|
inboxId ?? "unknown"
|
||||||
})`
|
})`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Lazy import: avoid ESM init cycles
|
|
||||||
const { monitorAgentMailProvider } = await import("./monitor.js");
|
const { monitorAgentMailProvider } = await import("./monitor.js");
|
||||||
return monitorAgentMailProvider({
|
return monitorAgentMailProvider({
|
||||||
accountId: account.accountId,
|
accountId,
|
||||||
abortSignal: ctx.abortSignal,
|
abortSignal: ctx.abortSignal,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,43 +2,24 @@ import type { AgentMailClient } from "agentmail";
|
|||||||
|
|
||||||
import type { AgentMailConfig } from "./utils.js";
|
import type { AgentMailConfig } from "./utils.js";
|
||||||
|
|
||||||
/**
|
/** Checks if a sender email matches any entry in a list (exact email or domain). */
|
||||||
* Checks if a sender email matches any entry in a list.
|
|
||||||
* Entries can be exact email addresses or domains.
|
|
||||||
*/
|
|
||||||
export function matchesList(senderEmail: string, list: string[]): boolean {
|
export function matchesList(senderEmail: string, list: string[]): boolean {
|
||||||
if (!list || list.length === 0) return false;
|
if (!list?.length) return false;
|
||||||
|
const sender = senderEmail.toLowerCase().trim();
|
||||||
const normalizedSender = senderEmail.toLowerCase().trim();
|
const domain = sender.split("@")[1];
|
||||||
const domain = normalizedSender.split("@")[1];
|
return list.some((e) => {
|
||||||
|
const entry = e.toLowerCase().trim();
|
||||||
return list.some((entry) => {
|
return entry === sender || entry === domain;
|
||||||
const normalizedEntry = entry.toLowerCase().trim();
|
|
||||||
// Match exact email or domain
|
|
||||||
return normalizedEntry === normalizedSender || normalizedEntry === domain;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Checks if sender is allowed: empty allowFrom = open mode (all allowed). */
|
||||||
* Determines if a sender is allowed based on allowFrom config.
|
|
||||||
*
|
|
||||||
* Logic:
|
|
||||||
* 1. If allowFrom is empty → allowed (open mode)
|
|
||||||
* 2. If sender matches allowFrom → allowed
|
|
||||||
* 3. Otherwise → not allowed
|
|
||||||
*/
|
|
||||||
export function checkSenderAllowed(
|
export function checkSenderAllowed(
|
||||||
senderEmail: string,
|
senderEmail: string,
|
||||||
config: Pick<AgentMailConfig, "allowFrom">,
|
config: Pick<AgentMailConfig, "allowFrom">
|
||||||
): boolean {
|
): boolean {
|
||||||
const { allowFrom = [] } = config;
|
const { allowFrom = [] } = config;
|
||||||
|
return allowFrom.length === 0 || matchesList(senderEmail, allowFrom);
|
||||||
// Open mode: all senders allowed when allowFrom is empty
|
|
||||||
if (allowFrom.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchesList(senderEmail, allowFrom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,7 +28,7 @@ export function checkSenderAllowed(
|
|||||||
export async function labelMessageAllowed(
|
export async function labelMessageAllowed(
|
||||||
client: AgentMailClient,
|
client: AgentMailClient,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
messageId: string,
|
messageId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await client.inboxes.messages.update(inboxId, messageId, {
|
await client.inboxes.messages.update(inboxId, messageId, {
|
||||||
addLabels: ["allowed"],
|
addLabels: ["allowed"],
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|||||||
|
|
||||||
import { registerPluginHttpRoute } from "clawdbot/plugin-sdk";
|
import { registerPluginHttpRoute } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js";
|
import { resolveCredentials } from "./accounts.js";
|
||||||
import { getAgentMailClient } from "./client.js";
|
import { getAgentMailClient } from "./client.js";
|
||||||
import { getAgentMailRuntime } from "./runtime.js";
|
import { getAgentMailRuntime } from "./runtime.js";
|
||||||
import { checkSenderAllowed, labelMessageAllowed } from "./filtering.js";
|
import { checkSenderAllowed, labelMessageAllowed } from "./filtering.js";
|
||||||
@ -16,30 +16,26 @@ export type MonitorAgentMailOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Runtime state tracking
|
// Runtime state tracking
|
||||||
const runtimeState = new Map<
|
type RuntimeState = {
|
||||||
string,
|
running: boolean;
|
||||||
{
|
lastStartAt: number | null;
|
||||||
running: boolean;
|
lastStopAt: number | null;
|
||||||
lastStartAt: number | null;
|
lastError: string | null;
|
||||||
lastStopAt: number | null;
|
lastInboundAt?: number | null;
|
||||||
lastError: string | null;
|
lastOutboundAt?: number | null;
|
||||||
lastInboundAt?: number | null;
|
};
|
||||||
lastOutboundAt?: number | null;
|
const runtimeState = new Map<string, RuntimeState>();
|
||||||
}
|
const defaultState: RuntimeState = {
|
||||||
>();
|
running: false,
|
||||||
|
lastStartAt: null,
|
||||||
|
lastStopAt: null,
|
||||||
|
lastError: null,
|
||||||
|
};
|
||||||
|
|
||||||
function recordState(
|
function recordState(accountId: string, state: Partial<RuntimeState>) {
|
||||||
accountId: string,
|
|
||||||
state: Partial<typeof runtimeState extends Map<string, infer V> ? V : never>
|
|
||||||
) {
|
|
||||||
const key = `agentmail:${accountId}`;
|
const key = `agentmail:${accountId}`;
|
||||||
runtimeState.set(key, {
|
runtimeState.set(key, {
|
||||||
...(runtimeState.get(key) ?? {
|
...(runtimeState.get(key) ?? defaultState),
|
||||||
running: false,
|
|
||||||
lastStartAt: null,
|
|
||||||
lastStopAt: null,
|
|
||||||
lastError: null,
|
|
||||||
}),
|
|
||||||
...state,
|
...state,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -87,27 +83,13 @@ export async function monitorAgentMailProvider(
|
|||||||
module: "agentmail-auto-reply",
|
module: "agentmail-auto-reply",
|
||||||
});
|
});
|
||||||
const logVerbose = (msg: string) => {
|
const logVerbose = (msg: string) => {
|
||||||
if (core.logging.shouldLogVerbose()) {
|
if (core.logging.shouldLogVerbose()) (logger.debug ?? logger.info)(msg);
|
||||||
if (logger.debug) {
|
|
||||||
logger.debug(msg);
|
|
||||||
} else {
|
|
||||||
logger.info(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const accountId = opts.accountId ?? "default";
|
const accountId = opts.accountId ?? "default";
|
||||||
const account = resolveAgentMailAccount({ cfg, accountId });
|
|
||||||
|
|
||||||
if (!account.configured) {
|
|
||||||
logger.warn("AgentMail not configured (missing token or email address)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKey, inboxId, webhookPath } = resolveCredentials(cfg);
|
const { apiKey, inboxId, webhookPath } = resolveCredentials(cfg);
|
||||||
|
|
||||||
if (!apiKey || !inboxId) {
|
if (!apiKey || !inboxId) {
|
||||||
logger.warn("AgentMail token or email address not found");
|
logger.warn("AgentMail not configured (missing token or email address)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,10 @@ import type {
|
|||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_WEBHOOK_PATH,
|
||||||
|
extractPathFromUrl,
|
||||||
resolveAgentMailAccount,
|
resolveAgentMailAccount,
|
||||||
resolveCredentials,
|
resolveCredentials,
|
||||||
resolveDefaultAgentMailAccountId,
|
|
||||||
} from "./accounts.js";
|
} from "./accounts.js";
|
||||||
import type { AgentMailConfig, CoreConfig } from "./utils.js";
|
import type { AgentMailConfig, CoreConfig } from "./utils.js";
|
||||||
|
|
||||||
@ -86,12 +87,9 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
configure: async ({ cfg, prompter, accountOverrides }) => {
|
configure: async ({ cfg, prompter, accountOverrides }) => {
|
||||||
const defaultAccountId = resolveDefaultAgentMailAccountId(
|
|
||||||
cfg as CoreConfig
|
|
||||||
);
|
|
||||||
const accountId = accountOverrides.agentmail
|
const accountId = accountOverrides.agentmail
|
||||||
? normalizeAccountId(accountOverrides.agentmail)
|
? normalizeAccountId(accountOverrides.agentmail)
|
||||||
: defaultAccountId;
|
: DEFAULT_ACCOUNT_ID;
|
||||||
|
|
||||||
let next = cfg as MoltbotConfig;
|
let next = cfg as MoltbotConfig;
|
||||||
const account = resolveAgentMailAccount({
|
const account = resolveAgentMailAccount({
|
||||||
@ -160,9 +158,6 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
// Apply config
|
// Apply config
|
||||||
next = updateAgentMailConfig(next, { enabled: true, token, emailAddress });
|
next = updateAgentMailConfig(next, { enabled: true, token, emailAddress });
|
||||||
|
|
||||||
// Webhook configuration
|
|
||||||
const DEFAULT_WEBHOOK_PATH = "/webhooks/agentmail";
|
|
||||||
|
|
||||||
// Ask if gateway has a public URL
|
// Ask if gateway has a public URL
|
||||||
const hasPublicUrl = await prompter.confirm({
|
const hasPublicUrl = await prompter.confirm({
|
||||||
message:
|
message:
|
||||||
@ -174,7 +169,6 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
let webhookPath = DEFAULT_WEBHOOK_PATH;
|
let webhookPath = DEFAULT_WEBHOOK_PATH;
|
||||||
|
|
||||||
if (hasPublicUrl) {
|
if (hasPublicUrl) {
|
||||||
// Get the full public webhook URL
|
|
||||||
webhookUrl = String(
|
webhookUrl = String(
|
||||||
await prompter.text({
|
await prompter.text({
|
||||||
message: "Full webhook URL",
|
message: "Full webhook URL",
|
||||||
@ -185,21 +179,13 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
if (
|
if (
|
||||||
!trimmed.startsWith("http://") &&
|
!trimmed.startsWith("http://") &&
|
||||||
!trimmed.startsWith("https://")
|
!trimmed.startsWith("https://")
|
||||||
) {
|
)
|
||||||
return "Must start with http:// or https://";
|
return "Must start with http:// or https://";
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
).trim();
|
).trim();
|
||||||
|
webhookPath = extractPathFromUrl(webhookUrl) ?? DEFAULT_WEBHOOK_PATH;
|
||||||
// Derive path from URL
|
|
||||||
try {
|
|
||||||
const parsed = new URL(webhookUrl);
|
|
||||||
webhookPath = parsed.pathname || DEFAULT_WEBHOOK_PATH;
|
|
||||||
} catch {
|
|
||||||
webhookPath = DEFAULT_WEBHOOK_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-register webhook with AgentMail
|
// Auto-register webhook with AgentMail
|
||||||
try {
|
try {
|
||||||
@ -349,22 +335,21 @@ async function promptForNewInbox(
|
|||||||
await prompter.note(`Your new inbox: ${emailAddress}`, "Inbox Created");
|
await prompter.note(`Your new inbox: ${emailAddress}`, "Inbox Created");
|
||||||
return emailAddress;
|
return emailAddress;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
const isConflict =
|
const lower = msg.toLowerCase();
|
||||||
message.toLowerCase().includes("already") ||
|
if (
|
||||||
message.toLowerCase().includes("taken") ||
|
lower.includes("already") ||
|
||||||
message.toLowerCase().includes("exists") ||
|
lower.includes("taken") ||
|
||||||
message.includes("409");
|
lower.includes("exists") ||
|
||||||
|
msg.includes("409")
|
||||||
if (isConflict) {
|
) {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
`${targetEmail} is already taken. Please try a different address.`,
|
`${targetEmail} is already taken. Please try a different address.`,
|
||||||
"Address Unavailable"
|
"Address Unavailable"
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
throw new Error(`Failed to create inbox: ${msg}`);
|
||||||
throw new Error(`Failed to create inbox: ${message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,62 +16,39 @@ export function extractMessageBody(
|
|||||||
return msg.extractedText ?? msg.extractedHtml ?? msg.text ?? msg.html ?? "";
|
return msg.extractedText ?? msg.extractedHtml ?? msg.text ?? msg.html ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats thread-level metadata.
|
|
||||||
*/
|
|
||||||
function formatThreadHeader(thread: Thread): string {
|
function formatThreadHeader(thread: Thread): string {
|
||||||
const lines: string[] = [];
|
return [
|
||||||
|
thread.subject && `Subject: ${thread.subject}`,
|
||||||
if (thread.subject) {
|
`Senders: ${thread.senders.join(", ")}`,
|
||||||
lines.push(`Subject: ${thread.subject}`);
|
`Recipients: ${thread.recipients.join(", ")}`,
|
||||||
}
|
`Messages: ${thread.messageCount}`,
|
||||||
|
]
|
||||||
lines.push(`Senders: ${thread.senders.join(", ")}`);
|
.filter(Boolean)
|
||||||
lines.push(`Recipients: ${thread.recipients.join(", ")}`);
|
.join("\n");
|
||||||
lines.push(`Messages: ${thread.messageCount}`);
|
|
||||||
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats recipients for a message (to, cc, bcc).
|
|
||||||
*/
|
|
||||||
function formatMessageRecipients(msg: Message): string {
|
function formatMessageRecipients(msg: Message): string {
|
||||||
const parts: string[] = [`To: ${msg.to.join(", ")}`];
|
return [
|
||||||
|
`To: ${msg.to.join(", ")}`,
|
||||||
if (msg.cc?.length) {
|
msg.cc?.length && `Cc: ${msg.cc.join(", ")}`,
|
||||||
parts.push(`Cc: ${msg.cc.join(", ")}`);
|
msg.bcc?.length && `Bcc: ${msg.bcc.join(", ")}`,
|
||||||
}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
if (msg.bcc?.length) {
|
.join("\n");
|
||||||
parts.push(`Bcc: ${msg.bcc.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a single message for context display.
|
|
||||||
*/
|
|
||||||
function formatMessage(msg: Message): string {
|
function formatMessage(msg: Message): string {
|
||||||
const recipients = formatMessageRecipients(msg);
|
|
||||||
const attachments = formatAttachments(msg.attachments);
|
const attachments = formatAttachments(msg.attachments);
|
||||||
const body = extractMessageBody(msg);
|
return [
|
||||||
const timestamp = formatUtcDate(msg.createdAt);
|
`--- ${formatUtcDate(msg.createdAt)} ---`,
|
||||||
|
|
||||||
const parts = [
|
|
||||||
`--- ${timestamp} ---`,
|
|
||||||
`From: ${msg.from}`,
|
`From: ${msg.from}`,
|
||||||
recipients,
|
formatMessageRecipients(msg),
|
||||||
];
|
attachments,
|
||||||
|
"",
|
||||||
if (attachments) {
|
extractMessageBody(msg),
|
||||||
parts.push(attachments);
|
]
|
||||||
}
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
parts.push("", body);
|
|
||||||
|
|
||||||
return parts.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user