@agenticmail/enterprise 0.5.48 → 0.5.50
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-263FEFGL.js +9085 -0
- package/dist/chunk-6T5PE7NL.js +12676 -0
- package/dist/chunk-JFXH5PXO.js +2115 -0
- package/dist/chunk-NUG7SMVQ.js +374 -0
- package/dist/chunk-R3JR6Z3H.js +898 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +737 -5
- package/dist/routes-XLP2NQ3M.js +5783 -0
- package/dist/runtime-NCYXXHGF.js +47 -0
- package/dist/server-U5SUFL4J.js +12 -0
- package/dist/setup-SIJZ4MNG.js +20 -0
- package/dist/skills-75VFQLP7.js +14 -0
- package/package.json +1 -1
- 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/engine/skills.ts +4 -257
- package/src/index.ts +5 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Graph Email Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements IEmailProvider using Microsoft Graph API.
|
|
5
|
+
* Agent authenticates via org's Azure AD / Entra ID 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 GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
|
|
16
|
+
|
|
17
|
+
export class MicrosoftEmailProvider implements IEmailProvider {
|
|
18
|
+
readonly provider: EmailProvider = 'microsoft';
|
|
19
|
+
private identity: AgentEmailIdentity | null = null;
|
|
20
|
+
|
|
21
|
+
private get token(): string {
|
|
22
|
+
if (!this.identity) throw new Error('Not connected');
|
|
23
|
+
return this.identity.accessToken;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async refreshIfNeeded(): Promise<void> {
|
|
27
|
+
if (this.identity?.refreshToken) {
|
|
28
|
+
this.identity.accessToken = await this.identity.refreshToken();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async graphFetch(path: string, opts?: RequestInit): Promise<any> {
|
|
33
|
+
await this.refreshIfNeeded();
|
|
34
|
+
const res = await fetch(`${GRAPH_BASE}${path}`, {
|
|
35
|
+
...opts,
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${this.token}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...opts?.headers,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const text = await res.text().catch(() => '');
|
|
44
|
+
throw new Error(`Graph API ${res.status}: ${text}`);
|
|
45
|
+
}
|
|
46
|
+
if (res.status === 204) return null;
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Connection ─────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
async connect(identity: AgentEmailIdentity): Promise<void> {
|
|
53
|
+
this.identity = identity;
|
|
54
|
+
// Validate token by fetching profile
|
|
55
|
+
await this.graphFetch('/me?$select=mail,displayName');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async disconnect(): Promise<void> {
|
|
59
|
+
this.identity = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── List / Read ────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
async listMessages(folder: string, opts?: { limit?: number; offset?: number }): Promise<EmailEnvelope[]> {
|
|
65
|
+
const folderId = this.resolveFolderId(folder);
|
|
66
|
+
const top = opts?.limit || 20;
|
|
67
|
+
const skip = opts?.offset || 0;
|
|
68
|
+
const data = await this.graphFetch(
|
|
69
|
+
`/me/mailFolders/${folderId}/messages?$top=${top}&$skip=${skip}&$select=id,subject,from,toRecipients,receivedDateTime,isRead,flag,hasAttachments,bodyPreview&$orderby=receivedDateTime desc`
|
|
70
|
+
);
|
|
71
|
+
return (data.value || []).map((m: any) => this.toEnvelope(m));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async readMessage(uid: string): Promise<EmailMessage> {
|
|
75
|
+
const data = await this.graphFetch(`/me/messages/${uid}?$select=id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,isRead,flag,hasAttachments,body,bodyPreview,replyTo,internetMessageId,internetMessageHeaders,conversationId`);
|
|
76
|
+
return this.toMessage(data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async searchMessages(criteria: SearchCriteria): Promise<EmailEnvelope[]> {
|
|
80
|
+
const filters: string[] = [];
|
|
81
|
+
if (criteria.from) filters.push(`from/emailAddress/address eq '${criteria.from}'`);
|
|
82
|
+
if (criteria.subject) filters.push(`contains(subject, '${criteria.subject}')`);
|
|
83
|
+
if (criteria.since) filters.push(`receivedDateTime ge ${criteria.since}`);
|
|
84
|
+
if (criteria.before) filters.push(`receivedDateTime lt ${criteria.before}`);
|
|
85
|
+
if (criteria.seen !== undefined) filters.push(`isRead eq ${criteria.seen}`);
|
|
86
|
+
|
|
87
|
+
let path = '/me/messages?$top=50&$select=id,subject,from,toRecipients,receivedDateTime,isRead,flag,hasAttachments,bodyPreview&$orderby=receivedDateTime desc';
|
|
88
|
+
if (filters.length) path += '&$filter=' + encodeURIComponent(filters.join(' and '));
|
|
89
|
+
if (criteria.text) path = `/me/messages?$search="${encodeURIComponent(criteria.text)}"&$top=50&$select=id,subject,from,toRecipients,receivedDateTime,isRead,flag,hasAttachments,bodyPreview`;
|
|
90
|
+
|
|
91
|
+
const data = await this.graphFetch(path);
|
|
92
|
+
return (data.value || []).map((m: any) => this.toEnvelope(m));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async listFolders(): Promise<EmailFolder[]> {
|
|
96
|
+
const data = await this.graphFetch('/me/mailFolders?$select=id,displayName,unreadItemCount,totalItemCount');
|
|
97
|
+
return (data.value || []).map((f: any) => ({
|
|
98
|
+
name: f.displayName,
|
|
99
|
+
path: f.id,
|
|
100
|
+
unread: f.unreadItemCount || 0,
|
|
101
|
+
total: f.totalItemCount || 0,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async createFolder(name: string): Promise<void> {
|
|
106
|
+
await this.graphFetch('/me/mailFolders', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify({ displayName: name }),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Send ───────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
async send(options: SendEmailOptions): Promise<{ messageId: string }> {
|
|
115
|
+
const message = this.buildGraphMessage(options);
|
|
116
|
+
await this.graphFetch('/me/sendMail', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: JSON.stringify({ message, saveToSentItems: true }),
|
|
119
|
+
});
|
|
120
|
+
return { messageId: `graph-${Date.now()}` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async reply(uid: string, body: string, replyAll = false): Promise<{ messageId: string }> {
|
|
124
|
+
const endpoint = replyAll ? 'replyAll' : 'reply';
|
|
125
|
+
await this.graphFetch(`/me/messages/${uid}/${endpoint}`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
body: JSON.stringify({ comment: body }),
|
|
128
|
+
});
|
|
129
|
+
return { messageId: `graph-reply-${Date.now()}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async forward(uid: string, to: string, body?: string): Promise<{ messageId: string }> {
|
|
133
|
+
await this.graphFetch(`/me/messages/${uid}/forward`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
comment: body || '',
|
|
137
|
+
toRecipients: [{ emailAddress: { address: to } }],
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
return { messageId: `graph-fwd-${Date.now()}` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Organize ───────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async moveMessage(uid: string, toFolder: string): Promise<void> {
|
|
146
|
+
const folderId = this.resolveFolderId(toFolder);
|
|
147
|
+
await this.graphFetch(`/me/messages/${uid}/move`, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: JSON.stringify({ destinationId: folderId }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async deleteMessage(uid: string): Promise<void> {
|
|
154
|
+
await this.graphFetch(`/me/messages/${uid}`, { method: 'DELETE' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async markRead(uid: string): Promise<void> {
|
|
158
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
159
|
+
method: 'PATCH',
|
|
160
|
+
body: JSON.stringify({ isRead: true }),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async markUnread(uid: string): Promise<void> {
|
|
165
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
166
|
+
method: 'PATCH',
|
|
167
|
+
body: JSON.stringify({ isRead: false }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async flagMessage(uid: string): Promise<void> {
|
|
172
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
173
|
+
method: 'PATCH',
|
|
174
|
+
body: JSON.stringify({ flag: { flagStatus: 'flagged' } }),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async unflagMessage(uid: string): Promise<void> {
|
|
179
|
+
await this.graphFetch(`/me/messages/${uid}`, {
|
|
180
|
+
method: 'PATCH',
|
|
181
|
+
body: JSON.stringify({ flag: { flagStatus: 'notFlagged' } }),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Batch ──────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
async batchMarkRead(uids: string[]): Promise<void> {
|
|
188
|
+
await Promise.all(uids.map(uid => this.markRead(uid)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async batchMarkUnread(uids: string[]): Promise<void> {
|
|
192
|
+
await Promise.all(uids.map(uid => this.markUnread(uid)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async batchMove(uids: string[], toFolder: string): Promise<void> {
|
|
196
|
+
await Promise.all(uids.map(uid => this.moveMessage(uid, toFolder)));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async batchDelete(uids: string[]): Promise<void> {
|
|
200
|
+
await Promise.all(uids.map(uid => this.deleteMessage(uid)));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Helpers ────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
private resolveFolderId(folder: string): string {
|
|
206
|
+
const map: Record<string, string> = {
|
|
207
|
+
INBOX: 'inbox', inbox: 'inbox',
|
|
208
|
+
Sent: 'sentItems', sent: 'sentItems', sentitems: 'sentItems',
|
|
209
|
+
Drafts: 'drafts', drafts: 'drafts',
|
|
210
|
+
Trash: 'deletedItems', trash: 'deletedItems', deleteditems: 'deletedItems',
|
|
211
|
+
Junk: 'junkemail', junk: 'junkemail', spam: 'junkemail',
|
|
212
|
+
Archive: 'archive', archive: 'archive',
|
|
213
|
+
};
|
|
214
|
+
return map[folder] || folder;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private buildGraphMessage(options: SendEmailOptions): any {
|
|
218
|
+
const msg: any = {
|
|
219
|
+
subject: options.subject,
|
|
220
|
+
body: { contentType: options.html ? 'HTML' : 'Text', content: options.html || options.body },
|
|
221
|
+
toRecipients: options.to.split(',').map(e => ({ emailAddress: { address: e.trim() } })),
|
|
222
|
+
};
|
|
223
|
+
if (options.cc) msg.ccRecipients = options.cc.split(',').map(e => ({ emailAddress: { address: e.trim() } }));
|
|
224
|
+
if (options.bcc) msg.bccRecipients = options.bcc.split(',').map(e => ({ emailAddress: { address: e.trim() } }));
|
|
225
|
+
if (options.inReplyTo) msg.internetMessageHeaders = [{ name: 'In-Reply-To', value: options.inReplyTo }];
|
|
226
|
+
return msg;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private toEnvelope(m: any): EmailEnvelope {
|
|
230
|
+
return {
|
|
231
|
+
uid: m.id,
|
|
232
|
+
from: { name: m.from?.emailAddress?.name, email: m.from?.emailAddress?.address || '' },
|
|
233
|
+
to: (m.toRecipients || []).map((r: any) => ({ name: r.emailAddress?.name, email: r.emailAddress?.address || '' })),
|
|
234
|
+
subject: m.subject || '',
|
|
235
|
+
date: m.receivedDateTime || '',
|
|
236
|
+
read: !!m.isRead,
|
|
237
|
+
flagged: m.flag?.flagStatus === 'flagged',
|
|
238
|
+
hasAttachments: !!m.hasAttachments,
|
|
239
|
+
preview: m.bodyPreview || '',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private toMessage(m: any): EmailMessage {
|
|
244
|
+
return {
|
|
245
|
+
uid: m.id,
|
|
246
|
+
from: { name: m.from?.emailAddress?.name, email: m.from?.emailAddress?.address || '' },
|
|
247
|
+
to: (m.toRecipients || []).map((r: any) => ({ name: r.emailAddress?.name, email: r.emailAddress?.address || '' })),
|
|
248
|
+
cc: (m.ccRecipients || []).map((r: any) => ({ name: r.emailAddress?.name, email: r.emailAddress?.address || '' })),
|
|
249
|
+
subject: m.subject || '',
|
|
250
|
+
body: m.body?.contentType === 'HTML' ? '' : (m.body?.content || ''),
|
|
251
|
+
html: m.body?.contentType === 'HTML' ? m.body?.content : undefined,
|
|
252
|
+
date: m.receivedDateTime || '',
|
|
253
|
+
read: !!m.isRead,
|
|
254
|
+
flagged: m.flag?.flagStatus === 'flagged',
|
|
255
|
+
folder: 'inbox',
|
|
256
|
+
messageId: m.internetMessageId,
|
|
257
|
+
attachments: [],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|