@agenticmail/enterprise 0.5.49 → 0.5.51
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/chunk-FKDN7ZV3.js +898 -0
- package/dist/chunk-G7BBCWAX.js +13428 -0
- package/dist/chunk-Q4WDMWLJ.js +2115 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +735 -3
- package/dist/runtime-ENGVD2AI.js +47 -0
- package/dist/server-JBOS22AY.js +12 -0
- package/dist/setup-6ATX2BNE.js +20 -0
- package/package.json +1 -14
- package/src/agent-tools/index.ts +22 -3
- package/src/agent-tools/tools/agenticmail.ts +785 -0
- package/src/agenticmail/index.ts +32 -0
- package/src/agenticmail/manager.ts +253 -0
- package/src/agenticmail/providers/google.ts +331 -0
- package/src/agenticmail/providers/index.ts +26 -0
- package/src/agenticmail/providers/microsoft.ts +260 -0
- package/src/agenticmail/types.ts +171 -0
- package/src/agenticmail-core/index.ts +36 -0
- package/src/agenticmail-core/pending-followup.ts +362 -0
- package/src/agenticmail-core/telemetry.ts +164 -0
- package/src/agenticmail-core/tools.ts +2395 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgenticMail Enterprise
|
|
3
|
+
*
|
|
4
|
+
* Embedded email & communication system for enterprise agents.
|
|
5
|
+
* No separate server — agents use their org email via OAuth.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { AgenticMailManager } from './agenticmail/index.js';
|
|
9
|
+
* const mail = new AgenticMailManager({ db: engineDb });
|
|
10
|
+
* await mail.registerAgent({ agentId, email, accessToken, provider: 'microsoft', ... });
|
|
11
|
+
* const provider = mail.getProvider(agentId);
|
|
12
|
+
* await provider.send({ to: 'user@company.com', subject: 'Hello', body: '...' });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export { AgenticMailManager } from './manager.js';
|
|
16
|
+
export type { AgenticMailManagerOptions } from './manager.js';
|
|
17
|
+
export { createEmailProvider } from './providers/index.js';
|
|
18
|
+
export { MicrosoftEmailProvider } from './providers/microsoft.js';
|
|
19
|
+
export { GoogleEmailProvider } from './providers/google.js';
|
|
20
|
+
export type {
|
|
21
|
+
IEmailProvider,
|
|
22
|
+
AgentEmailIdentity,
|
|
23
|
+
EmailProvider,
|
|
24
|
+
EmailMessage,
|
|
25
|
+
EmailEnvelope,
|
|
26
|
+
EmailFolder,
|
|
27
|
+
SendEmailOptions,
|
|
28
|
+
SearchCriteria,
|
|
29
|
+
EmailAttachment,
|
|
30
|
+
AgentMessage,
|
|
31
|
+
AgentTask,
|
|
32
|
+
} from './types.js';
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgenticMail Enterprise Manager
|
|
3
|
+
*
|
|
4
|
+
* Central orchestrator that connects agents to their org email.
|
|
5
|
+
* Manages email provider instances per agent and provides the
|
|
6
|
+
* tool handler interface for the enterprise agent system.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* Agent (from org directory via OAuth)
|
|
10
|
+
* → AgenticMailManager.getProvider(agentId)
|
|
11
|
+
* → IEmailProvider (Microsoft Graph / Gmail API)
|
|
12
|
+
* → Org's email system
|
|
13
|
+
*
|
|
14
|
+
* No separate AgenticMail server. No API keys. No relay setup.
|
|
15
|
+
* The agent IS the org identity.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { EngineDatabase } from '../engine/db-adapter.js';
|
|
19
|
+
import type {
|
|
20
|
+
IEmailProvider, AgentEmailIdentity, EmailProvider,
|
|
21
|
+
AgentMessage, AgentTask,
|
|
22
|
+
} from './types.js';
|
|
23
|
+
import { createEmailProvider } from './providers/index.js';
|
|
24
|
+
|
|
25
|
+
export interface AgenticMailManagerOptions {
|
|
26
|
+
db?: EngineDatabase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class AgenticMailManager {
|
|
30
|
+
private providers = new Map<string, IEmailProvider>();
|
|
31
|
+
private identities = new Map<string, AgentEmailIdentity>();
|
|
32
|
+
private db?: EngineDatabase;
|
|
33
|
+
|
|
34
|
+
constructor(opts?: AgenticMailManagerOptions) {
|
|
35
|
+
this.db = opts?.db;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setDb(db: EngineDatabase): void {
|
|
39
|
+
this.db = db;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Agent Registration ─────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register an agent's email identity from the org's OAuth/SSO.
|
|
46
|
+
* Called when an agent is created or when its OAuth token is refreshed.
|
|
47
|
+
*/
|
|
48
|
+
async registerAgent(identity: AgentEmailIdentity): Promise<void> {
|
|
49
|
+
this.identities.set(identity.agentId, identity);
|
|
50
|
+
|
|
51
|
+
// Create and connect the email provider
|
|
52
|
+
const provider = createEmailProvider(identity.provider);
|
|
53
|
+
await provider.connect(identity);
|
|
54
|
+
this.providers.set(identity.agentId, provider);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Unregister an agent (on deletion or token revocation).
|
|
59
|
+
*/
|
|
60
|
+
async unregisterAgent(agentId: string): Promise<void> {
|
|
61
|
+
const provider = this.providers.get(agentId);
|
|
62
|
+
if (provider) {
|
|
63
|
+
await provider.disconnect().catch(() => {});
|
|
64
|
+
this.providers.delete(agentId);
|
|
65
|
+
}
|
|
66
|
+
this.identities.delete(agentId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the email provider for an agent.
|
|
71
|
+
* Throws if agent is not registered.
|
|
72
|
+
*/
|
|
73
|
+
getProvider(agentId: string): IEmailProvider {
|
|
74
|
+
const provider = this.providers.get(agentId);
|
|
75
|
+
if (!provider) throw new Error(`Agent ${agentId} has no email provider registered. Ensure the agent has been connected via org OAuth.`);
|
|
76
|
+
return provider;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the email identity for an agent.
|
|
81
|
+
*/
|
|
82
|
+
getIdentity(agentId: string): AgentEmailIdentity | undefined {
|
|
83
|
+
return this.identities.get(agentId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if an agent has email access.
|
|
88
|
+
*/
|
|
89
|
+
hasEmail(agentId: string): boolean {
|
|
90
|
+
return this.providers.has(agentId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Inter-Agent Messaging ──────────────────────────
|
|
94
|
+
// These use the enterprise DB directly, not email.
|
|
95
|
+
// Agents in the same org can message each other without email.
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Send a message from one agent to another (internal, no email).
|
|
99
|
+
*/
|
|
100
|
+
async sendAgentMessage(from: string, to: string, subject: string, body: string, priority: 'normal' | 'high' | 'urgent' = 'normal'): Promise<AgentMessage> {
|
|
101
|
+
const msg: AgentMessage = {
|
|
102
|
+
id: crypto.randomUUID(),
|
|
103
|
+
from,
|
|
104
|
+
to,
|
|
105
|
+
subject,
|
|
106
|
+
body,
|
|
107
|
+
priority,
|
|
108
|
+
createdAt: new Date().toISOString(),
|
|
109
|
+
read: false,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (this.db) {
|
|
113
|
+
await this.db.execute(
|
|
114
|
+
`INSERT INTO agent_messages (id, from_agent, to_agent, subject, body, priority, created_at, read)
|
|
115
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0)`,
|
|
116
|
+
[msg.id, msg.from, msg.to, msg.subject, msg.body, msg.priority, msg.createdAt]
|
|
117
|
+
).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return msg;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get unread messages for an agent.
|
|
125
|
+
*/
|
|
126
|
+
async getAgentMessages(agentId: string, opts?: { unreadOnly?: boolean; limit?: number }): Promise<AgentMessage[]> {
|
|
127
|
+
if (!this.db) return [];
|
|
128
|
+
try {
|
|
129
|
+
let sql = 'SELECT * FROM agent_messages WHERE to_agent = ?';
|
|
130
|
+
const params: any[] = [agentId];
|
|
131
|
+
if (opts?.unreadOnly) {
|
|
132
|
+
sql += ' AND read = 0';
|
|
133
|
+
}
|
|
134
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
135
|
+
params.push(opts?.limit || 20);
|
|
136
|
+
|
|
137
|
+
const rows = await this.db.query<any>(sql, params);
|
|
138
|
+
return rows.map((r: any) => ({
|
|
139
|
+
id: r.id,
|
|
140
|
+
from: r.from_agent,
|
|
141
|
+
to: r.to_agent,
|
|
142
|
+
subject: r.subject,
|
|
143
|
+
body: r.body,
|
|
144
|
+
priority: r.priority,
|
|
145
|
+
createdAt: r.created_at,
|
|
146
|
+
read: !!r.read,
|
|
147
|
+
}));
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Task Management ────────────────────────────────
|
|
154
|
+
// Tasks also use the enterprise DB directly.
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a task assigned to an agent.
|
|
158
|
+
*/
|
|
159
|
+
async createTask(assigner: string, assignee: string, title: string, description?: string, priority: 'low' | 'normal' | 'high' | 'urgent' = 'normal'): Promise<AgentTask> {
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
const task: AgentTask = {
|
|
162
|
+
id: crypto.randomUUID(),
|
|
163
|
+
assigner,
|
|
164
|
+
assignee,
|
|
165
|
+
title,
|
|
166
|
+
description,
|
|
167
|
+
status: 'pending',
|
|
168
|
+
priority,
|
|
169
|
+
createdAt: now,
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (this.db) {
|
|
174
|
+
await this.db.execute(
|
|
175
|
+
`INSERT INTO agent_tasks (id, assigner, assignee, title, description, status, priority, created_at, updated_at)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
177
|
+
[task.id, task.assigner, task.assignee, task.title, task.description || null, task.status, task.priority, task.createdAt, task.updatedAt]
|
|
178
|
+
).catch(() => {});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return task;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get tasks for an agent.
|
|
186
|
+
*/
|
|
187
|
+
async getAgentTasks(agentId: string, direction: 'incoming' | 'outgoing' = 'incoming', status?: string): Promise<AgentTask[]> {
|
|
188
|
+
if (!this.db) return [];
|
|
189
|
+
try {
|
|
190
|
+
const col = direction === 'incoming' ? 'assignee' : 'assigner';
|
|
191
|
+
let sql = `SELECT * FROM agent_tasks WHERE ${col} = ?`;
|
|
192
|
+
const params: any[] = [agentId];
|
|
193
|
+
if (status) {
|
|
194
|
+
sql += ' AND status = ?';
|
|
195
|
+
params.push(status);
|
|
196
|
+
}
|
|
197
|
+
sql += ' ORDER BY created_at DESC LIMIT 50';
|
|
198
|
+
|
|
199
|
+
const rows = await this.db.query<any>(sql, params);
|
|
200
|
+
return rows.map((r: any) => ({
|
|
201
|
+
id: r.id,
|
|
202
|
+
assigner: r.assigner,
|
|
203
|
+
assignee: r.assignee,
|
|
204
|
+
title: r.title,
|
|
205
|
+
description: r.description,
|
|
206
|
+
status: r.status,
|
|
207
|
+
priority: r.priority,
|
|
208
|
+
result: r.result ? JSON.parse(r.result) : undefined,
|
|
209
|
+
createdAt: r.created_at,
|
|
210
|
+
updatedAt: r.updated_at,
|
|
211
|
+
}));
|
|
212
|
+
} catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Update task status.
|
|
219
|
+
*/
|
|
220
|
+
async updateTask(taskId: string, updates: { status?: string; result?: any }): Promise<void> {
|
|
221
|
+
if (!this.db) return;
|
|
222
|
+
const sets: string[] = ['updated_at = ?'];
|
|
223
|
+
const params: any[] = [new Date().toISOString()];
|
|
224
|
+
if (updates.status) { sets.push('status = ?'); params.push(updates.status); }
|
|
225
|
+
if (updates.result !== undefined) { sets.push('result = ?'); params.push(JSON.stringify(updates.result)); }
|
|
226
|
+
params.push(taskId);
|
|
227
|
+
await this.db.execute(`UPDATE agent_tasks SET ${sets.join(', ')} WHERE id = ?`, params).catch(() => {});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Lifecycle ──────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get all registered agents and their email status.
|
|
234
|
+
*/
|
|
235
|
+
getRegisteredAgents(): { agentId: string; email: string; provider: EmailProvider }[] {
|
|
236
|
+
const agents: { agentId: string; email: string; provider: EmailProvider }[] = [];
|
|
237
|
+
for (const [agentId, identity] of this.identities) {
|
|
238
|
+
agents.push({ agentId, email: identity.email, provider: identity.provider });
|
|
239
|
+
}
|
|
240
|
+
return agents;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Shutdown — disconnect all providers.
|
|
245
|
+
*/
|
|
246
|
+
async shutdown(): Promise<void> {
|
|
247
|
+
for (const provider of this.providers.values()) {
|
|
248
|
+
await provider.disconnect().catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
this.providers.clear();
|
|
251
|
+
this.identities.clear();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail API Email Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements IEmailProvider using Gmail REST API.
|
|
5
|
+
* Agent authenticates via org's Google Workspace OAuth.
|
|
6
|
+
* Email address comes from the org directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
IEmailProvider, AgentEmailIdentity, EmailProvider,
|
|
11
|
+
EmailMessage, EmailEnvelope, EmailFolder,
|
|
12
|
+
SendEmailOptions, SearchCriteria,
|
|
13
|
+
} from '../types.js';
|
|
14
|
+
|
|
15
|
+
const GMAIL_BASE = 'https://gmail.googleapis.com/gmail/v1';
|
|
16
|
+
|
|
17
|
+
export class GoogleEmailProvider implements IEmailProvider {
|
|
18
|
+
readonly provider: EmailProvider = 'google';
|
|
19
|
+
private identity: AgentEmailIdentity | null = null;
|
|
20
|
+
private userId = 'me';
|
|
21
|
+
|
|
22
|
+
private get token(): string {
|
|
23
|
+
if (!this.identity) throw new Error('Not connected');
|
|
24
|
+
return this.identity.accessToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async refreshIfNeeded(): Promise<void> {
|
|
28
|
+
if (this.identity?.refreshToken) {
|
|
29
|
+
this.identity.accessToken = await this.identity.refreshToken();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async gmailFetch(path: string, opts?: RequestInit): Promise<any> {
|
|
34
|
+
await this.refreshIfNeeded();
|
|
35
|
+
const res = await fetch(`${GMAIL_BASE}/users/${this.userId}${path}`, {
|
|
36
|
+
...opts,
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: `Bearer ${this.token}`,
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
...opts?.headers,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const text = await res.text().catch(() => '');
|
|
45
|
+
throw new Error(`Gmail API ${res.status}: ${text}`);
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 204) return null;
|
|
48
|
+
return res.json();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Connection ─────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async connect(identity: AgentEmailIdentity): Promise<void> {
|
|
54
|
+
this.identity = identity;
|
|
55
|
+
// Validate token
|
|
56
|
+
await this.gmailFetch('/profile');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async disconnect(): Promise<void> {
|
|
60
|
+
this.identity = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── List / Read ────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
async listMessages(folder: string, opts?: { limit?: number; offset?: number }): Promise<EmailEnvelope[]> {
|
|
66
|
+
const labelId = this.resolveLabelId(folder);
|
|
67
|
+
const maxResults = opts?.limit || 20;
|
|
68
|
+
const q = labelId === 'INBOX' ? '' : '';
|
|
69
|
+
const data = await this.gmailFetch(`/messages?labelIds=${labelId}&maxResults=${maxResults}${q ? '&q=' + encodeURIComponent(q) : ''}`);
|
|
70
|
+
|
|
71
|
+
if (!data.messages?.length) return [];
|
|
72
|
+
|
|
73
|
+
// Batch fetch message metadata
|
|
74
|
+
const envelopes: EmailEnvelope[] = [];
|
|
75
|
+
for (const msg of data.messages) {
|
|
76
|
+
try {
|
|
77
|
+
const detail = await this.gmailFetch(`/messages/${msg.id}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`);
|
|
78
|
+
envelopes.push(this.metadataToEnvelope(detail));
|
|
79
|
+
} catch { /* skip individual errors */ }
|
|
80
|
+
}
|
|
81
|
+
return envelopes;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async readMessage(uid: string): Promise<EmailMessage> {
|
|
85
|
+
const data = await this.gmailFetch(`/messages/${uid}?format=full`);
|
|
86
|
+
return this.fullToMessage(data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async searchMessages(criteria: SearchCriteria): Promise<EmailEnvelope[]> {
|
|
90
|
+
const parts: string[] = [];
|
|
91
|
+
if (criteria.from) parts.push(`from:${criteria.from}`);
|
|
92
|
+
if (criteria.to) parts.push(`to:${criteria.to}`);
|
|
93
|
+
if (criteria.subject) parts.push(`subject:${criteria.subject}`);
|
|
94
|
+
if (criteria.text) parts.push(criteria.text);
|
|
95
|
+
if (criteria.since) parts.push(`after:${criteria.since.split('T')[0]}`);
|
|
96
|
+
if (criteria.before) parts.push(`before:${criteria.before.split('T')[0]}`);
|
|
97
|
+
if (criteria.seen === true) parts.push('is:read');
|
|
98
|
+
if (criteria.seen === false) parts.push('is:unread');
|
|
99
|
+
|
|
100
|
+
const q = parts.join(' ');
|
|
101
|
+
const data = await this.gmailFetch(`/messages?q=${encodeURIComponent(q)}&maxResults=50`);
|
|
102
|
+
|
|
103
|
+
if (!data.messages?.length) return [];
|
|
104
|
+
|
|
105
|
+
const envelopes: EmailEnvelope[] = [];
|
|
106
|
+
for (const msg of data.messages.slice(0, 20)) {
|
|
107
|
+
try {
|
|
108
|
+
const detail = await this.gmailFetch(`/messages/${msg.id}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`);
|
|
109
|
+
envelopes.push(this.metadataToEnvelope(detail));
|
|
110
|
+
} catch { /* skip */ }
|
|
111
|
+
}
|
|
112
|
+
return envelopes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async listFolders(): Promise<EmailFolder[]> {
|
|
116
|
+
const data = await this.gmailFetch('/labels');
|
|
117
|
+
return (data.labels || []).map((l: any) => ({
|
|
118
|
+
name: l.name,
|
|
119
|
+
path: l.id,
|
|
120
|
+
unread: l.messagesUnread || 0,
|
|
121
|
+
total: l.messagesTotal || 0,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async createFolder(name: string): Promise<void> {
|
|
126
|
+
await this.gmailFetch('/labels', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: JSON.stringify({ name, labelListVisibility: 'labelShow', messageListVisibility: 'show' }),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Send ───────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async send(options: SendEmailOptions): Promise<{ messageId: string }> {
|
|
135
|
+
const raw = this.buildRawEmail(options);
|
|
136
|
+
const data = await this.gmailFetch('/messages/send', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
body: JSON.stringify({ raw }),
|
|
139
|
+
});
|
|
140
|
+
return { messageId: data.id };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async reply(uid: string, body: string, replyAll = false): Promise<{ messageId: string }> {
|
|
144
|
+
const original = await this.readMessage(uid);
|
|
145
|
+
const to = replyAll
|
|
146
|
+
? [original.from.email, ...(original.to || []).map(t => t.email), ...(original.cc || []).map(c => c.email)].filter(e => e !== this.identity?.email).join(', ')
|
|
147
|
+
: original.from.email;
|
|
148
|
+
|
|
149
|
+
return this.send({
|
|
150
|
+
to,
|
|
151
|
+
subject: original.subject.startsWith('Re:') ? original.subject : `Re: ${original.subject}`,
|
|
152
|
+
body,
|
|
153
|
+
inReplyTo: original.messageId,
|
|
154
|
+
references: original.references ? [...original.references, original.messageId!] : [original.messageId!],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async forward(uid: string, to: string, body?: string): Promise<{ messageId: string }> {
|
|
159
|
+
const original = await this.readMessage(uid);
|
|
160
|
+
return this.send({
|
|
161
|
+
to,
|
|
162
|
+
subject: `Fwd: ${original.subject}`,
|
|
163
|
+
body: (body ? body + '\n\n' : '') + `---------- Forwarded message ----------\nFrom: ${original.from.email}\nDate: ${original.date}\nSubject: ${original.subject}\n\n${original.body}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Organize ───────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
async moveMessage(uid: string, toFolder: string, fromFolder?: string): Promise<void> {
|
|
170
|
+
const addLabel = this.resolveLabelId(toFolder);
|
|
171
|
+
const removeLabel = fromFolder ? this.resolveLabelId(fromFolder) : 'INBOX';
|
|
172
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify({ addLabelIds: [addLabel], removeLabelIds: [removeLabel] }),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async deleteMessage(uid: string): Promise<void> {
|
|
179
|
+
await this.gmailFetch(`/messages/${uid}/trash`, { method: 'POST' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async markRead(uid: string): Promise<void> {
|
|
183
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
body: JSON.stringify({ removeLabelIds: ['UNREAD'] }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async markUnread(uid: string): Promise<void> {
|
|
190
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: JSON.stringify({ addLabelIds: ['UNREAD'] }),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async flagMessage(uid: string): Promise<void> {
|
|
197
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: JSON.stringify({ addLabelIds: ['STARRED'] }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async unflagMessage(uid: string): Promise<void> {
|
|
204
|
+
await this.gmailFetch(`/messages/${uid}/modify`, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
body: JSON.stringify({ removeLabelIds: ['STARRED'] }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Batch ──────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async batchMarkRead(uids: string[]): Promise<void> {
|
|
213
|
+
await this.gmailFetch('/messages/batchModify', {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
body: JSON.stringify({ ids: uids, removeLabelIds: ['UNREAD'] }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async batchMarkUnread(uids: string[]): Promise<void> {
|
|
220
|
+
await this.gmailFetch('/messages/batchModify', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
body: JSON.stringify({ ids: uids, addLabelIds: ['UNREAD'] }),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async batchMove(uids: string[], toFolder: string, fromFolder?: string): Promise<void> {
|
|
227
|
+
const addLabel = this.resolveLabelId(toFolder);
|
|
228
|
+
const removeLabel = fromFolder ? this.resolveLabelId(fromFolder) : 'INBOX';
|
|
229
|
+
await this.gmailFetch('/messages/batchModify', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
body: JSON.stringify({ ids: uids, addLabelIds: [addLabel], removeLabelIds: [removeLabel] }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async batchDelete(uids: string[]): Promise<void> {
|
|
236
|
+
await Promise.all(uids.map(uid => this.deleteMessage(uid)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Helpers ────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
private resolveLabelId(folder: string): string {
|
|
242
|
+
const map: Record<string, string> = {
|
|
243
|
+
INBOX: 'INBOX', inbox: 'INBOX',
|
|
244
|
+
Sent: 'SENT', sent: 'SENT',
|
|
245
|
+
Drafts: 'DRAFT', drafts: 'DRAFT',
|
|
246
|
+
Trash: 'TRASH', trash: 'TRASH',
|
|
247
|
+
Spam: 'SPAM', spam: 'SPAM', Junk: 'SPAM', junk: 'SPAM',
|
|
248
|
+
Starred: 'STARRED', starred: 'STARRED',
|
|
249
|
+
Important: 'IMPORTANT', important: 'IMPORTANT',
|
|
250
|
+
};
|
|
251
|
+
return map[folder] || folder;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private buildRawEmail(options: SendEmailOptions): string {
|
|
255
|
+
const lines = [
|
|
256
|
+
`To: ${options.to}`,
|
|
257
|
+
`Subject: ${options.subject}`,
|
|
258
|
+
`Content-Type: text/plain; charset=utf-8`,
|
|
259
|
+
];
|
|
260
|
+
if (options.cc) lines.splice(1, 0, `Cc: ${options.cc}`);
|
|
261
|
+
if (options.inReplyTo) lines.push(`In-Reply-To: ${options.inReplyTo}`);
|
|
262
|
+
if (options.references?.length) lines.push(`References: ${options.references.join(' ')}`);
|
|
263
|
+
lines.push('', options.body);
|
|
264
|
+
|
|
265
|
+
const raw = lines.join('\r\n');
|
|
266
|
+
// Base64url encode
|
|
267
|
+
return btoa(unescape(encodeURIComponent(raw)))
|
|
268
|
+
.replace(/\+/g, '-')
|
|
269
|
+
.replace(/\//g, '_')
|
|
270
|
+
.replace(/=+$/, '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getHeader(msg: any, name: string): string {
|
|
274
|
+
const headers = msg.payload?.headers || [];
|
|
275
|
+
const h = headers.find((h: any) => h.name.toLowerCase() === name.toLowerCase());
|
|
276
|
+
return h?.value || '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private metadataToEnvelope(msg: any): EmailEnvelope {
|
|
280
|
+
const from = this.getHeader(msg, 'From');
|
|
281
|
+
const fromMatch = from.match(/^(.*?)\s*<(.+?)>$/) || [null, '', from];
|
|
282
|
+
return {
|
|
283
|
+
uid: msg.id,
|
|
284
|
+
from: { name: fromMatch[1]?.replace(/"/g, '').trim() || undefined, email: fromMatch[2] || from },
|
|
285
|
+
to: [{ email: this.getHeader(msg, 'To') }],
|
|
286
|
+
subject: this.getHeader(msg, 'Subject'),
|
|
287
|
+
date: this.getHeader(msg, 'Date'),
|
|
288
|
+
read: !(msg.labelIds || []).includes('UNREAD'),
|
|
289
|
+
flagged: (msg.labelIds || []).includes('STARRED'),
|
|
290
|
+
hasAttachments: false,
|
|
291
|
+
preview: msg.snippet || '',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private fullToMessage(msg: any): EmailMessage {
|
|
296
|
+
const from = this.getHeader(msg, 'From');
|
|
297
|
+
const fromMatch = from.match(/^(.*?)\s*<(.+?)>$/) || [null, '', from];
|
|
298
|
+
|
|
299
|
+
// Extract body from parts
|
|
300
|
+
let body = '';
|
|
301
|
+
let html: string | undefined;
|
|
302
|
+
const extractBody = (payload: any) => {
|
|
303
|
+
if (payload.mimeType === 'text/plain' && payload.body?.data) {
|
|
304
|
+
body = Buffer.from(payload.body.data, 'base64url').toString('utf-8');
|
|
305
|
+
}
|
|
306
|
+
if (payload.mimeType === 'text/html' && payload.body?.data) {
|
|
307
|
+
html = Buffer.from(payload.body.data, 'base64url').toString('utf-8');
|
|
308
|
+
}
|
|
309
|
+
if (payload.parts) payload.parts.forEach(extractBody);
|
|
310
|
+
};
|
|
311
|
+
if (msg.payload) extractBody(msg.payload);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
uid: msg.id,
|
|
315
|
+
from: { name: fromMatch[1]?.replace(/"/g, '').trim() || undefined, email: fromMatch[2] || from },
|
|
316
|
+
to: [{ email: this.getHeader(msg, 'To') }],
|
|
317
|
+
cc: this.getHeader(msg, 'Cc') ? [{ email: this.getHeader(msg, 'Cc') }] : undefined,
|
|
318
|
+
subject: this.getHeader(msg, 'Subject'),
|
|
319
|
+
body,
|
|
320
|
+
html,
|
|
321
|
+
date: this.getHeader(msg, 'Date'),
|
|
322
|
+
read: !(msg.labelIds || []).includes('UNREAD'),
|
|
323
|
+
flagged: (msg.labelIds || []).includes('STARRED'),
|
|
324
|
+
folder: (msg.labelIds || []).includes('INBOX') ? 'inbox' : 'other',
|
|
325
|
+
messageId: this.getHeader(msg, 'Message-ID'),
|
|
326
|
+
inReplyTo: this.getHeader(msg, 'In-Reply-To') || undefined,
|
|
327
|
+
references: this.getHeader(msg, 'References') ? this.getHeader(msg, 'References').split(/\s+/) : undefined,
|
|
328
|
+
attachments: [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates the right email provider based on the org's identity provider.
|
|
5
|
+
* - Microsoft 365 / Azure AD → Microsoft Graph API
|
|
6
|
+
* - Google Workspace → Gmail API
|
|
7
|
+
* - Generic IMAP/SMTP → IMAP provider (future)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { MicrosoftEmailProvider } from './microsoft.js';
|
|
11
|
+
export { GoogleEmailProvider } from './google.js';
|
|
12
|
+
|
|
13
|
+
import type { IEmailProvider, EmailProvider } from '../types.js';
|
|
14
|
+
import { MicrosoftEmailProvider } from './microsoft.js';
|
|
15
|
+
import { GoogleEmailProvider } from './google.js';
|
|
16
|
+
|
|
17
|
+
export function createEmailProvider(provider: EmailProvider): IEmailProvider {
|
|
18
|
+
switch (provider) {
|
|
19
|
+
case 'microsoft': return new MicrosoftEmailProvider();
|
|
20
|
+
case 'google': return new GoogleEmailProvider();
|
|
21
|
+
case 'imap':
|
|
22
|
+
throw new Error('Generic IMAP provider not yet implemented — use Microsoft or Google');
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`Unknown email provider: ${provider}`);
|
|
25
|
+
}
|
|
26
|
+
}
|