@aipost/mcp-server
Advanced tools
+49
-13
@@ -10,13 +10,23 @@ /** | ||
| * | ||
| * Every blacklist / whitelist entry and every sender address normalises to | ||
| * { keyname, alias }. Four input formats are accepted: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * Supported address formats: | ||
| * | ||
| * AIPost addresses: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * | ||
| * Standard email addresses (for external senders / domain filtering): | ||
| * 5. user@domain.tld (full email — exact match) | ||
| * 6. @domain.tld (domain-only — matches any user@domain.tld) | ||
| * | ||
| * Matching rules: | ||
| * - alias must always match (case-insensitive). | ||
| * - If the filter entry specifies a keyname, the sender's keyname must also | ||
| * match. If the filter entry omits keyname, any keyname is accepted. | ||
| * - For AIPost addresses: alias must always match (case-insensitive). | ||
| * If the filter entry specifies a keyname, the sender's keyname must also | ||
| * match. If the filter entry omits keyname, any keyname is accepted. | ||
| * - For standard emails: full case-insensitive exact match on the email. | ||
| * - For domain-only patterns (@domain.tld): matches any sender whose address | ||
| * ends with @domain.tld. | ||
| * - For EXTERNAL_EMAIL messages (IMAP imports): the filter checks both | ||
| * `sender` (display name) and `payload.from` (actual email address). | ||
| */ | ||
@@ -30,4 +40,15 @@ export interface ParsedAddress { | ||
| /** | ||
| * Parse an AIPost email address in any of the 4 supported formats. | ||
| * Returns null when the address doesn't look like an AIPost address. | ||
| * Parse an address in any supported format. | ||
| * | ||
| * AIPost formats: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * | ||
| * Standard email formats (for external senders / domain-level filtering): | ||
| * 5. user@domain.tld (full email) | ||
| * 6. @domain.tld (domain-only — used in filter entries) | ||
| * | ||
| * Returns null when the address is unparseable (e.g. a bare display name). | ||
| */ | ||
@@ -47,3 +68,14 @@ export declare function parseAddress(raw: string): ParsedAddress | null; | ||
| /** | ||
| * Filter an array of objects that have a `sender` field. | ||
| * Check whether a mail item is allowed, considering both `sender` and | ||
| * `payload.from` (external emails store the real address in payload.from). | ||
| */ | ||
| isItemAllowed(item: { | ||
| sender?: string; | ||
| payload?: { | ||
| from?: string; | ||
| }; | ||
| }): boolean; | ||
| /** | ||
| * Filter an array of objects that have a `sender` field (and optionally | ||
| * `payload.from` for external emails). | ||
| * Returns a new array with blocked senders removed. | ||
@@ -53,2 +85,5 @@ */ | ||
| sender?: string; | ||
| payload?: { | ||
| from?: string; | ||
| }; | ||
| }>(items: T[]): T[]; | ||
@@ -70,3 +105,4 @@ /** | ||
| /** | ||
| * Filter SSE events. Each event's `data` may contain a `sender` field. | ||
| * Filter SSE events. Each event's `data` may contain a `sender` field | ||
| * and/or `payload.from` (external emails). | ||
| */ | ||
@@ -73,0 +109,0 @@ filterEvents<T extends { |
+85
-26
@@ -10,13 +10,23 @@ /** | ||
| * | ||
| * Every blacklist / whitelist entry and every sender address normalises to | ||
| * { keyname, alias }. Four input formats are accepted: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * Supported address formats: | ||
| * | ||
| * AIPost addresses: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * | ||
| * Standard email addresses (for external senders / domain filtering): | ||
| * 5. user@domain.tld (full email — exact match) | ||
| * 6. @domain.tld (domain-only — matches any user@domain.tld) | ||
| * | ||
| * Matching rules: | ||
| * - alias must always match (case-insensitive). | ||
| * - If the filter entry specifies a keyname, the sender's keyname must also | ||
| * match. If the filter entry omits keyname, any keyname is accepted. | ||
| * - For AIPost addresses: alias must always match (case-insensitive). | ||
| * If the filter entry specifies a keyname, the sender's keyname must also | ||
| * match. If the filter entry omits keyname, any keyname is accepted. | ||
| * - For standard emails: full case-insensitive exact match on the email. | ||
| * - For domain-only patterns (@domain.tld): matches any sender whose address | ||
| * ends with @domain.tld. | ||
| * - For EXTERNAL_EMAIL messages (IMAP imports): the filter checks both | ||
| * `sender` (display name) and `payload.from` (actual email address). | ||
| */ | ||
@@ -26,4 +36,15 @@ const WHITELIST_ENV = "AIPOST_SENDER_WHITELIST"; | ||
| /** | ||
| * Parse an AIPost email address in any of the 4 supported formats. | ||
| * Returns null when the address doesn't look like an AIPost address. | ||
| * Parse an address in any supported format. | ||
| * | ||
| * AIPost formats: | ||
| * 1. alias.aipost.email (short dot) | ||
| * 2. keyname.alias.aipost.email (full dot) | ||
| * 3. alias@aipost.email (short at) | ||
| * 4. keyname.alias@aipost.email (full at) | ||
| * | ||
| * Standard email formats (for external senders / domain-level filtering): | ||
| * 5. user@domain.tld (full email) | ||
| * 6. @domain.tld (domain-only — used in filter entries) | ||
| * | ||
| * Returns null when the address is unparseable (e.g. a bare display name). | ||
| */ | ||
@@ -34,3 +55,3 @@ export function parseAddress(raw) { | ||
| return null; | ||
| // At-format: alias@aipost.email or keyname.alias@aipost.email | ||
| // At-format | ||
| const atIdx = s.indexOf("@"); | ||
@@ -40,5 +61,13 @@ if (atIdx !== -1) { | ||
| const domain = s.slice(atIdx + 1); | ||
| if (domain !== "aipost.email") | ||
| return null; | ||
| return parseLocal(local); | ||
| // AIPost address: alias@aipost.email or keyname.alias@aipost.email | ||
| if (domain === "aipost.email") { | ||
| return parseLocal(local); | ||
| } | ||
| // Standard email: user@domain.tld or domain-only pattern: @domain.tld | ||
| if (domain.includes(".")) { | ||
| // local is empty for @domain.tld patterns; non-empty for user@domain.tld | ||
| return { alias: s, keyname: null }; | ||
| } | ||
| // Bare domain without TLD (e.g. "user@localhost") — not useful for filtering | ||
| return null; | ||
| } | ||
@@ -125,3 +154,24 @@ // Dot-format: alias.aipost.email or keyname.alias.aipost.email | ||
| /** | ||
| * Filter an array of objects that have a `sender` field. | ||
| * Check whether a mail item is allowed, considering both `sender` and | ||
| * `payload.from` (external emails store the real address in payload.from). | ||
| */ | ||
| isItemAllowed(item) { | ||
| if (!this.active) | ||
| return true; | ||
| // Check sender field first | ||
| if (item.sender && this.isAllowed(item.sender)) | ||
| return true; | ||
| // Fallback: for EXTERNAL_EMAIL, the sender field may be a display name; | ||
| // the actual email address is in payload.from. | ||
| if (item.payload?.from && this.isAllowed(item.payload.from)) | ||
| return true; | ||
| // If we have any sender info but neither check passed, block | ||
| if (item.sender || item.payload?.from) | ||
| return false; | ||
| // No sender info at all — allow through | ||
| return true; | ||
| } | ||
| /** | ||
| * Filter an array of objects that have a `sender` field (and optionally | ||
| * `payload.from` for external emails). | ||
| * Returns a new array with blocked senders removed. | ||
@@ -132,7 +182,3 @@ */ | ||
| return items; | ||
| return items.filter((item) => { | ||
| if (!item.sender) | ||
| return true; // no sender field — allow through | ||
| return this.isAllowed(item.sender); | ||
| }); | ||
| return items.filter((item) => this.isItemAllowed(item)); | ||
| } | ||
@@ -166,3 +212,4 @@ /** | ||
| /** | ||
| * Filter SSE events. Each event's `data` may contain a `sender` field. | ||
| * Filter SSE events. Each event's `data` may contain a `sender` field | ||
| * and/or `payload.from` (external emails). | ||
| */ | ||
@@ -175,6 +222,9 @@ filterEvents(events) { | ||
| return true; | ||
| const sender = event.data.sender; | ||
| if (typeof sender !== "string") | ||
| return true; | ||
| return this.isAllowed(sender); | ||
| const d = event.data; | ||
| const sender = typeof d.sender === "string" ? d.sender : undefined; | ||
| const payload = d.payload && typeof d.payload === "object" | ||
| ? d.payload | ||
| : undefined; | ||
| const from = payload && typeof payload.from === "string" ? payload.from : undefined; | ||
| return this.isItemAllowed({ sender, payload: from ? { from } : undefined }); | ||
| }); | ||
@@ -207,2 +257,11 @@ } | ||
| function entryMatches(entry, sender) { | ||
| // ── Domain-only pattern: @domain.tld matches any user@domain.tld ────── | ||
| if (entry.alias.startsWith("@")) { | ||
| return sender.alias.endsWith(entry.alias); | ||
| } | ||
| // ── Standard email matching (both contain @, non-AIPost) ────────────── | ||
| if (entry.alias.includes("@") && sender.alias.includes("@")) { | ||
| return entry.alias === sender.alias; | ||
| } | ||
| // ── AIPost address matching ──────────────────────────────────────────── | ||
| // alias must always match (case-insensitive — already lowercased) | ||
@@ -209,0 +268,0 @@ if (entry.alias !== sender.alias) |
+8
-8
@@ -234,5 +234,5 @@ import { createRequire } from "module"; | ||
| result = await client.getMessage(args.messageId); | ||
| // Block if sender is filtered | ||
| if (senderFilter.active && result?.sender) { | ||
| if (!senderFilter.isAllowed(result.sender)) { | ||
| // Block if sender is filtered (checks both sender and payload.from) | ||
| if (senderFilter.active) { | ||
| if (!senderFilter.isItemAllowed(result)) { | ||
| throw new Error(`Message ${args.messageId} not found`); | ||
@@ -290,5 +290,5 @@ } | ||
| result = await client.getThread(args.threadId); | ||
| // Filter messages in thread by sender | ||
| // Filter messages in thread by sender (checks both sender and payload.from) | ||
| if (senderFilter.active && Array.isArray(result)) { | ||
| result = senderFilter.filterBySender(result.map((m) => ({ ...m, sender: m.sender }))); | ||
| result = senderFilter.filterBySender(result); | ||
| } | ||
@@ -299,5 +299,5 @@ break; | ||
| result = await client.deleteMessage(args.messageId); | ||
| // Filter response sender | ||
| if (senderFilter.active && result?.sender) { | ||
| if (!senderFilter.isAllowed(result.sender)) { | ||
| // Filter response sender (checks both sender and payload.from) | ||
| if (senderFilter.active) { | ||
| if (!senderFilter.isItemAllowed(result)) { | ||
| throw new Error(`Message ${args.messageId} not found`); | ||
@@ -304,0 +304,0 @@ } |
+1
-1
| { | ||
| "name": "@aipost/mcp-server", | ||
| "version": "1.1.5", | ||
| "version": "1.1.6", | ||
| "mcpName": "io.github.AIPOST-EMAIL/mcp-server", | ||
@@ -5,0 +5,0 @@ "description": "MCP Server for AIPost.email — typed, structured messaging for AI agents with Ed25519 identities", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
94926
4.84%1949
5.12%