@mindstone-engineering/mcp-server-email-imap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.d.ts +16 -0
- package/dist/bridge.js +43 -0
- package/dist/imap-client.d.ts +21 -0
- package/dist/imap-client.js +144 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +45 -0
- package/dist/presets.d.ts +10 -0
- package/dist/presets.js +56 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +14 -0
- package/dist/smtp-client.d.ts +15 -0
- package/dist/smtp-client.js +61 -0
- package/dist/tools/configure.d.ts +12 -0
- package/dist/tools/configure.js +104 -0
- package/dist/tools/index.d.ts +32 -0
- package/dist/tools/index.js +76 -0
- package/dist/tools/mailbox.d.ts +6 -0
- package/dist/tools/mailbox.js +101 -0
- package/dist/tools/messages.d.ts +6 -0
- package/dist/tools/messages.js +212 -0
- package/dist/tools/send.d.ts +6 -0
- package/dist/tools/send.js +131 -0
- package/dist/tools/shared.d.ts +62 -0
- package/dist/tools/shared.js +187 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +12 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +42 -0
- package/package.json +51 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for email-imap tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { getConnection } from '../imap-client.js';
|
|
6
|
+
import { getPreset } from '../presets.js';
|
|
7
|
+
/**
|
|
8
|
+
* In-memory client config. Set by initClients(), read by tool handlers.
|
|
9
|
+
*/
|
|
10
|
+
let clientConfig = null;
|
|
11
|
+
export function setClientConfig(config) {
|
|
12
|
+
clientConfig = config ? { ...config } : null;
|
|
13
|
+
}
|
|
14
|
+
export function getClientConfig() {
|
|
15
|
+
return clientConfig;
|
|
16
|
+
}
|
|
17
|
+
export function ensureInitialized() {
|
|
18
|
+
if (!clientConfig) {
|
|
19
|
+
throw new Error('Email clients are not initialized');
|
|
20
|
+
}
|
|
21
|
+
return clientConfig;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Format email addresses for display.
|
|
25
|
+
*/
|
|
26
|
+
export function formatAddresses(addresses) {
|
|
27
|
+
if (!addresses || addresses.length === 0) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
return addresses
|
|
31
|
+
.map((address) => {
|
|
32
|
+
if (!address.address) {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
if (address.name) {
|
|
36
|
+
return `${address.name} <${address.address}>`;
|
|
37
|
+
}
|
|
38
|
+
return address.address;
|
|
39
|
+
})
|
|
40
|
+
.filter((entry) => entry.length > 0)
|
|
41
|
+
.join(', ');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format a date value to ISO string.
|
|
45
|
+
*/
|
|
46
|
+
export function formatDate(date) {
|
|
47
|
+
if (!date) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (date instanceof Date) {
|
|
51
|
+
return date.toISOString();
|
|
52
|
+
}
|
|
53
|
+
const parsed = new Date(date);
|
|
54
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate a unique Message-ID for an email.
|
|
58
|
+
*/
|
|
59
|
+
export function generateMessageId(email) {
|
|
60
|
+
const [, domain = 'localhost'] = email.split('@');
|
|
61
|
+
return `<${randomUUID()}@${domain}>`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read a stream into a Buffer.
|
|
65
|
+
*/
|
|
66
|
+
export async function streamToBuffer(stream) {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
for await (const chunk of stream) {
|
|
69
|
+
if (typeof chunk === 'string') {
|
|
70
|
+
chunks.push(Buffer.from(chunk));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
chunks.push(chunk);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Buffer.concat(chunks);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Download a message part as text by UID and part number.
|
|
80
|
+
*/
|
|
81
|
+
export async function downloadPartAsText(uid, part) {
|
|
82
|
+
const client = await getConnection();
|
|
83
|
+
const partData = await client.download(uid, part, { uid: true });
|
|
84
|
+
const content = await streamToBuffer(partData.content);
|
|
85
|
+
return content.toString('utf8');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Recursively collect text/html parts and attachments from a message structure.
|
|
89
|
+
*/
|
|
90
|
+
export function collectMessageParts(node, parts) {
|
|
91
|
+
if (!node) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(node.childNodes) && node.childNodes.length > 0) {
|
|
95
|
+
for (const childNode of node.childNodes) {
|
|
96
|
+
collectMessageParts(childNode, parts);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const contentType = node.type.toLowerCase();
|
|
101
|
+
const partIdentifier = node.part ?? '1';
|
|
102
|
+
if (contentType === 'text/plain' && !parts.textPart) {
|
|
103
|
+
parts.textPart = partIdentifier;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (contentType === 'text/html' && !parts.htmlPart) {
|
|
107
|
+
parts.htmlPart = partIdentifier;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const disposition = node.disposition?.toLowerCase();
|
|
111
|
+
const filename = node.dispositionParameters?.filename ?? node.parameters?.name ?? null;
|
|
112
|
+
const isAttachment = disposition === 'attachment' ||
|
|
113
|
+
(disposition === 'inline' && Boolean(filename)) ||
|
|
114
|
+
(Boolean(filename) && !contentType.startsWith('text/'));
|
|
115
|
+
if (!isAttachment) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
parts.attachments.push({
|
|
119
|
+
filename,
|
|
120
|
+
contentType,
|
|
121
|
+
size: typeof node.size === 'number' ? node.size : 0,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Remove duplicates from an array, preserving order (case-insensitive).
|
|
126
|
+
*/
|
|
127
|
+
export function uniquePreserveOrder(values) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
const result = [];
|
|
130
|
+
for (const value of values) {
|
|
131
|
+
const key = value.toLowerCase();
|
|
132
|
+
if (seen.has(key)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
seen.add(key);
|
|
136
|
+
result.push(value);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the drafts mailbox name for the current account.
|
|
142
|
+
*/
|
|
143
|
+
export async function resolveDraftsMailbox() {
|
|
144
|
+
const client = await getConnection();
|
|
145
|
+
const listedMailboxes = await client.list();
|
|
146
|
+
const mailboxByLowerName = new Map();
|
|
147
|
+
for (const mailbox of listedMailboxes) {
|
|
148
|
+
mailboxByLowerName.set(mailbox.path.toLowerCase(), mailbox.path);
|
|
149
|
+
}
|
|
150
|
+
const exactDrafts = mailboxByLowerName.get('drafts');
|
|
151
|
+
if (exactDrafts) {
|
|
152
|
+
return exactDrafts;
|
|
153
|
+
}
|
|
154
|
+
const specialUseDrafts = listedMailboxes.find((mailbox) => mailbox.specialUse === '\\Drafts');
|
|
155
|
+
if (specialUseDrafts) {
|
|
156
|
+
return specialUseDrafts.path;
|
|
157
|
+
}
|
|
158
|
+
const iCloudPreset = getPreset('icloud');
|
|
159
|
+
const yahooPreset = getPreset('yahoo');
|
|
160
|
+
const fallbackCandidates = uniquePreserveOrder([
|
|
161
|
+
'Drafts',
|
|
162
|
+
'Draft',
|
|
163
|
+
...(iCloudPreset?.folderFallbacks.drafts ?? []),
|
|
164
|
+
...(yahooPreset?.folderFallbacks.drafts ?? []),
|
|
165
|
+
]);
|
|
166
|
+
for (const candidate of fallbackCandidates) {
|
|
167
|
+
const existing = mailboxByLowerName.get(candidate.toLowerCase());
|
|
168
|
+
if (existing) {
|
|
169
|
+
return existing;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const defaultMailbox = fallbackCandidates[0] ?? 'Drafts';
|
|
173
|
+
await client.mailboxCreate(defaultMailbox);
|
|
174
|
+
return defaultMailbox;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Ensure a mailbox exists, creating it if necessary.
|
|
178
|
+
*/
|
|
179
|
+
export async function ensureMailboxExists(mailbox) {
|
|
180
|
+
const client = await getConnection();
|
|
181
|
+
const listedMailboxes = await client.list();
|
|
182
|
+
const exists = listedMailboxes.some((entry) => entry.path.toLowerCase() === mailbox.toLowerCase());
|
|
183
|
+
if (!exists) {
|
|
184
|
+
await client.mailboxCreate(mailbox);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=shared.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
2
|
+
export interface BridgeState {
|
|
3
|
+
port: number;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class EmailImapError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly resolution: string;
|
|
9
|
+
constructor(message: string, code: string, resolution: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Provider preset configuration for well-known email providers.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProviderPreset {
|
|
15
|
+
name: string;
|
|
16
|
+
imapHost: string;
|
|
17
|
+
imapPort: number;
|
|
18
|
+
imapTls: boolean;
|
|
19
|
+
smtpHost: string;
|
|
20
|
+
smtpPort: number;
|
|
21
|
+
smtpSecure: boolean;
|
|
22
|
+
authType: 'app-password';
|
|
23
|
+
folderFallbacks: Record<string, string[]>;
|
|
24
|
+
quirks?: string[];
|
|
25
|
+
emailDomains: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Full client configuration for initializing IMAP and SMTP connections.
|
|
29
|
+
*/
|
|
30
|
+
export interface ClientConfig {
|
|
31
|
+
imapHost: string;
|
|
32
|
+
imapPort: number;
|
|
33
|
+
imapTls: boolean;
|
|
34
|
+
smtpHost: string;
|
|
35
|
+
smtpPort: number;
|
|
36
|
+
smtpSecure: boolean;
|
|
37
|
+
email: string;
|
|
38
|
+
password: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* IMAP client configuration subset.
|
|
42
|
+
*/
|
|
43
|
+
export interface ImapClientConfig {
|
|
44
|
+
host: string;
|
|
45
|
+
port: number;
|
|
46
|
+
tls: boolean;
|
|
47
|
+
user: string;
|
|
48
|
+
pass: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* SMTP client configuration subset.
|
|
52
|
+
*/
|
|
53
|
+
export interface SmtpClientConfig {
|
|
54
|
+
host: string;
|
|
55
|
+
port: number;
|
|
56
|
+
secure: boolean;
|
|
57
|
+
user: string;
|
|
58
|
+
pass: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Interface for a mail transporter (abstracted for testing).
|
|
62
|
+
*/
|
|
63
|
+
export interface MailTransporter {
|
|
64
|
+
sendMail(mailOptions: Record<string, unknown>): Promise<{
|
|
65
|
+
messageId?: string;
|
|
66
|
+
}>;
|
|
67
|
+
close(): void;
|
|
68
|
+
verify?: () => Promise<boolean>;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
2
|
+
export class EmailImapError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
resolution;
|
|
5
|
+
constructor(message, code, resolution) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.resolution = resolution;
|
|
9
|
+
this.name = 'EmailImapError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a tool handler with standard error handling.
|
|
5
|
+
*
|
|
6
|
+
* - On success: returns the string result as a text content block.
|
|
7
|
+
* - On EmailImapError: returns a structured JSON error with code and resolution.
|
|
8
|
+
* - On unknown error: returns a generic error message.
|
|
9
|
+
*
|
|
10
|
+
* Secrets are never exposed in error messages.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EmailImapError } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a tool handler with standard error handling.
|
|
4
|
+
*
|
|
5
|
+
* - On success: returns the string result as a text content block.
|
|
6
|
+
* - On EmailImapError: returns a structured JSON error with code and resolution.
|
|
7
|
+
* - On unknown error: returns a generic error message.
|
|
8
|
+
*
|
|
9
|
+
* Secrets are never exposed in error messages.
|
|
10
|
+
*/
|
|
11
|
+
export function withErrorHandling(fn) {
|
|
12
|
+
return async (args, extra) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await fn(args, extra);
|
|
15
|
+
return { content: [{ type: 'text', text: result }] };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof EmailImapError) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
ok: false,
|
|
25
|
+
error: error.message,
|
|
26
|
+
code: error.code,
|
|
27
|
+
resolution: error.resolution,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=utils.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindstone-engineering/mcp-server-email-imap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Email IMAP/SMTP MCP server for Model Context Protocol hosts — supports iCloud Mail, Yahoo Mail, and custom IMAP providers",
|
|
5
|
+
"license": "FSL-1.1-MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server-email-imap": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"!dist/**/*.map"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/nspr-io/mcp-servers.git",
|
|
17
|
+
"directory": "connectors/email-imap"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/email-imap",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && shx chmod +x dist/index.js",
|
|
25
|
+
"prepare": "npm run build",
|
|
26
|
+
"watch": "tsc --watch",
|
|
27
|
+
"start": "node dist/index.js",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"test:coverage": "vitest run --coverage"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
34
|
+
"imapflow": "^1.2.0",
|
|
35
|
+
"nodemailer": "^6.9.0",
|
|
36
|
+
"zod": "^3.23.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
|
|
40
|
+
"@types/node": "^22",
|
|
41
|
+
"@types/nodemailer": "^6.4.0",
|
|
42
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
43
|
+
"msw": "^2.13.2",
|
|
44
|
+
"shx": "^0.3.4",
|
|
45
|
+
"typescript": "^5.8.2",
|
|
46
|
+
"vitest": "^4.1.3"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
}
|
|
51
|
+
}
|