@devosurf/tesser-connectors 0.1.0-alpha.2 → 0.1.0-alpha.3
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/README.md +2 -0
- package/manifest.json +42 -1
- package/outlook-mail/index.ts +273 -61
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
The Tesser connector library. **Apache-2.0** — connectors are permissive so the community and agents can contribute and embed them freely (ADR-0009). Never compile a connector into the AGPL server.
|
|
4
4
|
|
|
5
5
|
> Status: **implemented.** P0 set: `github`, `slack`, `http`, `resend`, `gmail`, `google-calendar`, plus the Microsoft Graph-backed `outlook-mail` mailbox connector. `pnpm codegen` assembles the registry (`index.ts`), the Catalog (`catalog/index.ts`) from provider declarations under `providers/`, and `manifest.json` — never hand-edit those three. Colocated `index.test.ts` runs against a fake fetch via `invokeAction`/`fakeFetch` from `@devosurf/tesser-testing`; `index.live.test.ts` suites hit real APIs (`pnpm test:live`, credentials from env, self-skipping).
|
|
6
|
+
>
|
|
7
|
+
> `outlook-mail` supports delegated Microsoft 365 user mailboxes by default and shared mailbox operations via the optional `mailbox` input, which targets Graph `/users/{userPrincipalName}` endpoints. It includes message list/get/send/reply/move/read-state/category helpers, draft create/reply/update/send, attachment get/list, mail folder list, master category list/create, and a delta-query `messageReceived` poll trigger. Graph webhook subscriptions remain future work because the runtime must own `validationToken` echoing and renewal.
|
|
6
8
|
|
|
7
9
|
A connector is **our own** typed artifact — no Nango, no third-party OAuth code (ADR-0004). One connector per directory (`connectors/<name>/index.ts` + colocated tests + `sampleData`); a codegen step owns the registry, so you never hand-edit a global file. Auth is declared **once** and injected into every Action as a pre-authed `ctx.http` client — the author **never names a token** (`ctx.auth` is a masked escape hatch for odd placements). Actions are nested typed calls that return *our* stable shape, which `output` validates — never the raw provider response (ADR-0012):
|
|
8
10
|
|
package/manifest.json
CHANGED
|
@@ -608,7 +608,12 @@
|
|
|
608
608
|
"offline_access",
|
|
609
609
|
"Mail.Read",
|
|
610
610
|
"Mail.ReadWrite",
|
|
611
|
-
"Mail.Send"
|
|
611
|
+
"Mail.Send",
|
|
612
|
+
"Mail.Read.Shared",
|
|
613
|
+
"Mail.ReadWrite.Shared",
|
|
614
|
+
"Mail.Send.Shared",
|
|
615
|
+
"MailboxSettings.Read",
|
|
616
|
+
"MailboxSettings.ReadWrite"
|
|
612
617
|
],
|
|
613
618
|
"flow": "auth_code",
|
|
614
619
|
"provider": "microsoft"
|
|
@@ -621,6 +626,12 @@
|
|
|
621
626
|
"retrySafe": true,
|
|
622
627
|
"describe": "List messages in a folder using Microsoft Graph's stable mapped shape"
|
|
623
628
|
},
|
|
629
|
+
{
|
|
630
|
+
"path": "messages.listConversation",
|
|
631
|
+
"safety": "read",
|
|
632
|
+
"retrySafe": true,
|
|
633
|
+
"describe": "List messages in the same Outlook conversation"
|
|
634
|
+
},
|
|
624
635
|
{
|
|
625
636
|
"path": "messages.get",
|
|
626
637
|
"safety": "read",
|
|
@@ -645,6 +656,24 @@
|
|
|
645
656
|
"retrySafe": true,
|
|
646
657
|
"describe": "Mark a message read (sets isRead=true)"
|
|
647
658
|
},
|
|
659
|
+
{
|
|
660
|
+
"path": "messages.setCategories",
|
|
661
|
+
"safety": "write",
|
|
662
|
+
"retrySafe": true,
|
|
663
|
+
"describe": "Replace a message's Outlook categories"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"path": "messages.addCategories",
|
|
667
|
+
"safety": "write",
|
|
668
|
+
"retrySafe": true,
|
|
669
|
+
"describe": "Add Outlook categories to a message without removing existing categories"
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"path": "messages.removeCategories",
|
|
673
|
+
"safety": "write",
|
|
674
|
+
"retrySafe": true,
|
|
675
|
+
"describe": "Remove Outlook categories from a message"
|
|
676
|
+
},
|
|
648
677
|
{
|
|
649
678
|
"path": "messages.move",
|
|
650
679
|
"safety": "write",
|
|
@@ -692,6 +721,18 @@
|
|
|
692
721
|
"safety": "read",
|
|
693
722
|
"retrySafe": true,
|
|
694
723
|
"describe": "List root mail folders (well-known folders like inbox, archive, deleteditems can be used directly)"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"path": "categories.list",
|
|
727
|
+
"safety": "read",
|
|
728
|
+
"retrySafe": true,
|
|
729
|
+
"describe": "List the mailbox's Outlook master categories"
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
"path": "categories.create",
|
|
733
|
+
"safety": "write",
|
|
734
|
+
"retrySafe": false,
|
|
735
|
+
"describe": "Create an Outlook master category"
|
|
695
736
|
}
|
|
696
737
|
],
|
|
697
738
|
"triggers": [
|
package/outlook-mail/index.ts
CHANGED
|
@@ -77,11 +77,13 @@ const attachmentShape = z.object({
|
|
|
77
77
|
isInline: z.boolean(),
|
|
78
78
|
});
|
|
79
79
|
const attachmentDetailShape = attachmentShape.extend({ contentBytes: z.string() });
|
|
80
|
+
const categoryShape = z.object({ id: z.string(), displayName: z.string(), color: z.string() });
|
|
80
81
|
|
|
82
|
+
const mailboxInput = z.object({ mailbox: z.string().min(1).optional() });
|
|
81
83
|
const recipientsInput = z.union([z.string().min(3), z.array(z.string().min(3)).min(1)]);
|
|
82
84
|
const bodyContentTypeInput = z.enum(["text", "html"]).default("text");
|
|
83
85
|
|
|
84
|
-
const
|
|
86
|
+
const composeInput = mailboxInput.extend({
|
|
85
87
|
to: recipientsInput,
|
|
86
88
|
subject: z.string(),
|
|
87
89
|
text: z.string(),
|
|
@@ -89,11 +91,14 @@ const sendOrDraftInput = z.object({
|
|
|
89
91
|
cc: recipientsInput.optional(),
|
|
90
92
|
bcc: recipientsInput.optional(),
|
|
91
93
|
replyTo: recipientsInput.optional(),
|
|
94
|
+
from: z.string().min(3).optional(),
|
|
92
95
|
categories: z.array(z.string()).optional(),
|
|
93
96
|
importance: z.enum(["low", "normal", "high"]).optional(),
|
|
94
97
|
});
|
|
95
98
|
|
|
96
|
-
const
|
|
99
|
+
const sendInput = composeInput.extend({ saveToSentItems: z.boolean().optional() });
|
|
100
|
+
|
|
101
|
+
const updateDraftInput = mailboxInput.extend({
|
|
97
102
|
id: z.string().min(1),
|
|
98
103
|
to: recipientsInput.optional(),
|
|
99
104
|
subject: z.string().optional(),
|
|
@@ -102,6 +107,7 @@ const updateDraftInput = z.object({
|
|
|
102
107
|
cc: recipientsInput.optional(),
|
|
103
108
|
bcc: recipientsInput.optional(),
|
|
104
109
|
replyTo: recipientsInput.optional(),
|
|
110
|
+
from: z.string().min(3).optional(),
|
|
105
111
|
categories: z.array(z.string()).optional(),
|
|
106
112
|
importance: z.enum(["low", "normal", "high"]).optional(),
|
|
107
113
|
});
|
|
@@ -148,6 +154,8 @@ type RawAttachment = {
|
|
|
148
154
|
isInline?: boolean;
|
|
149
155
|
contentBytes?: string;
|
|
150
156
|
};
|
|
157
|
+
type RawCategory = { id: string; displayName?: string; color?: string };
|
|
158
|
+
type DeltaCursor = { mailbox?: string; folderId: string; link: string; seeded: boolean };
|
|
151
159
|
type GraphCollection<T> = {
|
|
152
160
|
value?: T[];
|
|
153
161
|
"@odata.nextLink"?: string;
|
|
@@ -225,13 +233,17 @@ function mapAttachmentDetail(raw: RawAttachment): z.infer<typeof attachmentDetai
|
|
|
225
233
|
return { ...mapAttachment(raw), contentBytes: raw.contentBytes ?? "" };
|
|
226
234
|
}
|
|
227
235
|
|
|
236
|
+
function mapCategory(raw: RawCategory): z.infer<typeof categoryShape> {
|
|
237
|
+
return { id: raw.id, displayName: raw.displayName ?? "", color: raw.color ?? "" };
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
function recipientObjects(input: z.infer<typeof recipientsInput> | undefined): RawRecipient[] | undefined {
|
|
229
241
|
if (input === undefined) return undefined;
|
|
230
242
|
const values = Array.isArray(input) ? input : [input];
|
|
231
243
|
return values.map((address) => ({ emailAddress: { address } }));
|
|
232
244
|
}
|
|
233
245
|
|
|
234
|
-
function messagePayload(input: z.infer<typeof
|
|
246
|
+
function messagePayload(input: z.infer<typeof composeInput>): Record<string, unknown> {
|
|
235
247
|
return {
|
|
236
248
|
subject: input.subject,
|
|
237
249
|
body: { contentType: input.html === true ? "HTML" : "Text", content: input.text },
|
|
@@ -239,6 +251,7 @@ function messagePayload(input: z.infer<typeof sendOrDraftInput>): Record<string,
|
|
|
239
251
|
...(input.cc !== undefined ? { ccRecipients: recipientObjects(input.cc) } : {}),
|
|
240
252
|
...(input.bcc !== undefined ? { bccRecipients: recipientObjects(input.bcc) } : {}),
|
|
241
253
|
...(input.replyTo !== undefined ? { replyTo: recipientObjects(input.replyTo) } : {}),
|
|
254
|
+
...(input.from !== undefined ? { from: { emailAddress: { address: input.from } } } : {}),
|
|
242
255
|
...(input.categories !== undefined ? { categories: input.categories } : {}),
|
|
243
256
|
...(input.importance !== undefined ? { importance: input.importance } : {}),
|
|
244
257
|
};
|
|
@@ -252,17 +265,26 @@ function draftPatch(input: z.infer<typeof updateDraftInput>): Record<string, unk
|
|
|
252
265
|
...(input.cc !== undefined ? { ccRecipients: recipientObjects(input.cc) } : {}),
|
|
253
266
|
...(input.bcc !== undefined ? { bccRecipients: recipientObjects(input.bcc) } : {}),
|
|
254
267
|
...(input.replyTo !== undefined ? { replyTo: recipientObjects(input.replyTo) } : {}),
|
|
268
|
+
...(input.from !== undefined ? { from: { emailAddress: { address: input.from } } } : {}),
|
|
255
269
|
...(input.categories !== undefined ? { categories: input.categories } : {}),
|
|
256
270
|
...(input.importance !== undefined ? { importance: input.importance } : {}),
|
|
257
271
|
};
|
|
258
272
|
}
|
|
259
273
|
|
|
260
|
-
function
|
|
261
|
-
return `/
|
|
274
|
+
function mailboxRoot(mailbox?: string): string {
|
|
275
|
+
return mailbox !== undefined ? `/users/${encodeURIComponent(mailbox)}` : "/me";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function messagePath(id: string, mailbox?: string): string {
|
|
279
|
+
return `${mailboxRoot(mailbox)}/messages/${encodeURIComponent(id)}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function folderMessagesPath(folderId: string, mailbox?: string): string {
|
|
283
|
+
return `${mailboxRoot(mailbox)}/mailFolders/${encodeURIComponent(folderId)}/messages`;
|
|
262
284
|
}
|
|
263
285
|
|
|
264
|
-
function
|
|
265
|
-
return
|
|
286
|
+
function masterCategoriesPath(mailbox?: string): string {
|
|
287
|
+
return `${mailboxRoot(mailbox)}/outlook/masterCategories`;
|
|
266
288
|
}
|
|
267
289
|
|
|
268
290
|
async function listCollection<T>(
|
|
@@ -273,7 +295,7 @@ async function listCollection<T>(
|
|
|
273
295
|
): Promise<T[]> {
|
|
274
296
|
const out: T[] = [];
|
|
275
297
|
let path = firstPath;
|
|
276
|
-
for (let page = 0; page <
|
|
298
|
+
for (let page = 0; page < 50 && out.length < limit; page++) {
|
|
277
299
|
const res = (await ctx.http.get(path, page === 0 ? { query } : undefined)) as GraphCollection<T>;
|
|
278
300
|
out.push(...(res.value ?? []));
|
|
279
301
|
if (!res["@odata.nextLink"]) return out.slice(0, limit);
|
|
@@ -283,37 +305,80 @@ async function listCollection<T>(
|
|
|
283
305
|
throw new RetryableError(`microsoft graph pagination exceeded safety cap for ${firstPath}`);
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
function escapeODataString(value: string): string {
|
|
309
|
+
return value.replace(/'/g, "''");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function cursorMatches(cursor: DeltaCursor, mailbox: string | undefined, folderId: string): boolean {
|
|
313
|
+
return (cursor.mailbox ?? undefined) === mailbox && cursor.folderId === folderId;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function cursorFromString(cursor: string, mailbox: string | undefined, folderId: string): DeltaCursor | undefined {
|
|
317
|
+
if (mailbox !== undefined) return undefined; // legacy alpha.2 cursors only targeted /me.
|
|
318
|
+
try {
|
|
319
|
+
const path = new URL(cursor).pathname;
|
|
320
|
+
if (!path.includes(`/mailFolders/${encodeURIComponent(folderId)}/messages/delta`)) return undefined;
|
|
321
|
+
} catch {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
return { folderId, link: cursor, seeded: true };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeCursor(cursor: unknown, mailbox: string | undefined, folderId: string): DeltaCursor | undefined {
|
|
328
|
+
if (typeof cursor === "string" && cursor.length > 0) return cursorFromString(cursor, mailbox, folderId);
|
|
329
|
+
if (cursor && typeof cursor === "object") {
|
|
330
|
+
const c = cursor as Partial<DeltaCursor>;
|
|
331
|
+
if (typeof c.folderId === "string" && typeof c.link === "string" && typeof c.seeded === "boolean") {
|
|
332
|
+
const normalized: DeltaCursor = {
|
|
333
|
+
...(typeof c.mailbox === "string" ? { mailbox: c.mailbox } : {}),
|
|
334
|
+
folderId: c.folderId,
|
|
335
|
+
link: c.link,
|
|
336
|
+
seeded: c.seeded,
|
|
337
|
+
};
|
|
338
|
+
return cursorMatches(normalized, mailbox, folderId) ? normalized : undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
286
344
|
async function pollCreatedMessages(
|
|
287
345
|
ctx: ActionCtx,
|
|
346
|
+
mailbox: string | undefined,
|
|
288
347
|
folderId: string,
|
|
289
348
|
maxPageSize: number,
|
|
290
349
|
cursor: unknown,
|
|
291
|
-
): Promise<{ items: RawMessage[]; nextCursor?:
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
): Promise<{ items: RawMessage[]; nextCursor?: DeltaCursor }> {
|
|
351
|
+
const current = normalizeCursor(cursor, mailbox, folderId);
|
|
352
|
+
const first = current === undefined;
|
|
353
|
+
const res = (await ctx.http.get(current?.link ?? `${folderMessagesPath(folderId, mailbox)}/delta`, {
|
|
354
|
+
...(first
|
|
355
|
+
? {
|
|
356
|
+
query: {
|
|
357
|
+
changeType: "created",
|
|
358
|
+
$select: SUMMARY_SELECT,
|
|
359
|
+
$orderby: "receivedDateTime desc",
|
|
360
|
+
$top: maxPageSize,
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
: {}),
|
|
364
|
+
headers: { Prefer: `odata.maxpagesize=${maxPageSize}` },
|
|
365
|
+
})) as GraphCollection<RawMessage>;
|
|
366
|
+
const seeded = current?.seeded ?? false;
|
|
367
|
+
const rawItems = (res.value ?? []).filter((m) => m["@removed"] === undefined);
|
|
368
|
+
const cursorTarget = { ...(mailbox !== undefined ? { mailbox } : {}), folderId };
|
|
369
|
+
if (res["@odata.deltaLink"] !== undefined) {
|
|
370
|
+
return {
|
|
371
|
+
items: seeded ? rawItems : [],
|
|
372
|
+
nextCursor: { ...cursorTarget, link: res["@odata.deltaLink"], seeded: true },
|
|
373
|
+
};
|
|
315
374
|
}
|
|
316
|
-
|
|
375
|
+
if (res["@odata.nextLink"] !== undefined) {
|
|
376
|
+
return {
|
|
377
|
+
items: seeded ? rawItems : [],
|
|
378
|
+
nextCursor: { ...cursorTarget, link: res["@odata.nextLink"], seeded },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { items: seeded ? rawItems : [] };
|
|
317
382
|
}
|
|
318
383
|
|
|
319
384
|
function preferBody(contentType: "text" | "html", allowUnsafeHtml?: boolean): string {
|
|
@@ -329,7 +394,17 @@ export default defineConnector({
|
|
|
329
394
|
baseUrl: "https://graph.microsoft.com/v1.0",
|
|
330
395
|
auth: oauth2({
|
|
331
396
|
provider: "microsoft",
|
|
332
|
-
scopes: [
|
|
397
|
+
scopes: [
|
|
398
|
+
"offline_access",
|
|
399
|
+
"Mail.Read",
|
|
400
|
+
"Mail.ReadWrite",
|
|
401
|
+
"Mail.Send",
|
|
402
|
+
"Mail.Read.Shared",
|
|
403
|
+
"Mail.ReadWrite.Shared",
|
|
404
|
+
"Mail.Send.Shared",
|
|
405
|
+
"MailboxSettings.Read",
|
|
406
|
+
"MailboxSettings.ReadWrite",
|
|
407
|
+
],
|
|
333
408
|
}),
|
|
334
409
|
defaultHeaders: {
|
|
335
410
|
accept: "application/json",
|
|
@@ -352,6 +427,22 @@ export default defineConnector({
|
|
|
352
427
|
importance: "normal",
|
|
353
428
|
},
|
|
354
429
|
],
|
|
430
|
+
"messages.listConversation": [
|
|
431
|
+
{
|
|
432
|
+
id: "AAMkAD1",
|
|
433
|
+
conversationId: "AAQkAD1",
|
|
434
|
+
parentFolderId: "inbox",
|
|
435
|
+
from: "ada@example.com",
|
|
436
|
+
fromName: "Ada",
|
|
437
|
+
subject: "sample subject",
|
|
438
|
+
bodyPreview: "hello",
|
|
439
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
440
|
+
isRead: false,
|
|
441
|
+
hasAttachments: false,
|
|
442
|
+
categories: [],
|
|
443
|
+
importance: "normal",
|
|
444
|
+
},
|
|
445
|
+
],
|
|
355
446
|
"messages.get": {
|
|
356
447
|
id: "AAMkAD1",
|
|
357
448
|
conversationId: "AAQkAD1",
|
|
@@ -390,6 +481,48 @@ export default defineConnector({
|
|
|
390
481
|
categories: [],
|
|
391
482
|
importance: "normal",
|
|
392
483
|
},
|
|
484
|
+
"messages.setCategories": {
|
|
485
|
+
id: "AAMkAD1",
|
|
486
|
+
conversationId: "AAQkAD1",
|
|
487
|
+
parentFolderId: "inbox",
|
|
488
|
+
from: "ada@example.com",
|
|
489
|
+
fromName: "Ada",
|
|
490
|
+
subject: "sample subject",
|
|
491
|
+
bodyPreview: "hello",
|
|
492
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
493
|
+
isRead: true,
|
|
494
|
+
hasAttachments: false,
|
|
495
|
+
categories: ["Support"],
|
|
496
|
+
importance: "normal",
|
|
497
|
+
},
|
|
498
|
+
"messages.addCategories": {
|
|
499
|
+
id: "AAMkAD1",
|
|
500
|
+
conversationId: "AAQkAD1",
|
|
501
|
+
parentFolderId: "inbox",
|
|
502
|
+
from: "ada@example.com",
|
|
503
|
+
fromName: "Ada",
|
|
504
|
+
subject: "sample subject",
|
|
505
|
+
bodyPreview: "hello",
|
|
506
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
507
|
+
isRead: true,
|
|
508
|
+
hasAttachments: false,
|
|
509
|
+
categories: ["Support", "Escalated"],
|
|
510
|
+
importance: "normal",
|
|
511
|
+
},
|
|
512
|
+
"messages.removeCategories": {
|
|
513
|
+
id: "AAMkAD1",
|
|
514
|
+
conversationId: "AAQkAD1",
|
|
515
|
+
parentFolderId: "inbox",
|
|
516
|
+
from: "ada@example.com",
|
|
517
|
+
fromName: "Ada",
|
|
518
|
+
subject: "sample subject",
|
|
519
|
+
bodyPreview: "hello",
|
|
520
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
521
|
+
isRead: true,
|
|
522
|
+
hasAttachments: false,
|
|
523
|
+
categories: ["Support"],
|
|
524
|
+
importance: "normal",
|
|
525
|
+
},
|
|
393
526
|
"messages.move": {
|
|
394
527
|
id: "AAMkAD2",
|
|
395
528
|
conversationId: "AAQkAD1",
|
|
@@ -490,6 +623,8 @@ export default defineConnector({
|
|
|
490
623
|
isHidden: false,
|
|
491
624
|
},
|
|
492
625
|
],
|
|
626
|
+
"categories.list": [{ id: "cat1", displayName: "Support", color: "preset0" }],
|
|
627
|
+
"categories.create": { id: "cat2", displayName: "Escalated", color: "preset1" },
|
|
493
628
|
"trigger:messageReceived": {
|
|
494
629
|
id: "AAMkAD3",
|
|
495
630
|
conversationId: "AAQkAD3",
|
|
@@ -509,7 +644,7 @@ export default defineConnector({
|
|
|
509
644
|
messages: {
|
|
510
645
|
list: action({
|
|
511
646
|
describe: "List messages in a folder using Microsoft Graph's stable mapped shape",
|
|
512
|
-
input:
|
|
647
|
+
input: mailboxInput.extend({
|
|
513
648
|
folderId: z.string().default("inbox"),
|
|
514
649
|
filter: z.string().optional(),
|
|
515
650
|
maxResults: z.number().int().min(1).max(250).default(25),
|
|
@@ -519,7 +654,7 @@ export default defineConnector({
|
|
|
519
654
|
run: async (ctx, i) => {
|
|
520
655
|
const raw = await listCollection<RawMessage>(
|
|
521
656
|
ctx,
|
|
522
|
-
folderMessagesPath(i.folderId),
|
|
657
|
+
folderMessagesPath(i.folderId, i.mailbox),
|
|
523
658
|
{
|
|
524
659
|
$select: SUMMARY_SELECT,
|
|
525
660
|
$orderby: "receivedDateTime desc",
|
|
@@ -531,9 +666,33 @@ export default defineConnector({
|
|
|
531
666
|
return raw.map(mapSummary);
|
|
532
667
|
},
|
|
533
668
|
}),
|
|
669
|
+
listConversation: action({
|
|
670
|
+
describe: "List messages in the same Outlook conversation",
|
|
671
|
+
input: mailboxInput.extend({
|
|
672
|
+
conversationId: z.string().min(1),
|
|
673
|
+
folderId: z.string().optional(),
|
|
674
|
+
maxResults: z.number().int().min(1).max(250).default(50),
|
|
675
|
+
}),
|
|
676
|
+
output: z.array(messageSummaryShape),
|
|
677
|
+
safety: "read",
|
|
678
|
+
run: async (ctx, i) => {
|
|
679
|
+
const raw = await listCollection<RawMessage>(
|
|
680
|
+
ctx,
|
|
681
|
+
i.folderId !== undefined ? folderMessagesPath(i.folderId, i.mailbox) : `${mailboxRoot(i.mailbox)}/messages`,
|
|
682
|
+
{
|
|
683
|
+
$select: SUMMARY_SELECT,
|
|
684
|
+
$filter: `conversationId eq '${escapeODataString(i.conversationId)}'`,
|
|
685
|
+
$orderby: "receivedDateTime desc",
|
|
686
|
+
$top: Math.min(i.maxResults, 100),
|
|
687
|
+
},
|
|
688
|
+
i.maxResults,
|
|
689
|
+
);
|
|
690
|
+
return raw.map(mapSummary);
|
|
691
|
+
},
|
|
692
|
+
}),
|
|
534
693
|
get: action({
|
|
535
694
|
describe: "Fetch one message with body, recipients, categories, and thread ids",
|
|
536
|
-
input:
|
|
695
|
+
input: mailboxInput.extend({
|
|
537
696
|
id: z.string().min(1),
|
|
538
697
|
bodyContentType: bodyContentTypeInput,
|
|
539
698
|
allowUnsafeHtml: z.boolean().optional(),
|
|
@@ -541,7 +700,7 @@ export default defineConnector({
|
|
|
541
700
|
output: messageDetailShape,
|
|
542
701
|
safety: "read",
|
|
543
702
|
run: async (ctx, i) => {
|
|
544
|
-
const raw = (await ctx.http.get(messagePath(i.id), {
|
|
703
|
+
const raw = (await ctx.http.get(messagePath(i.id, i.mailbox), {
|
|
545
704
|
query: { $select: DETAIL_SELECT },
|
|
546
705
|
headers: { Prefer: preferBody(i.bodyContentType, i.allowUnsafeHtml) },
|
|
547
706
|
})) as RawMessage;
|
|
@@ -550,23 +709,26 @@ export default defineConnector({
|
|
|
550
709
|
}),
|
|
551
710
|
send: action({
|
|
552
711
|
describe: "Send a new email using Outlook Mail (not idempotent; creates Sent Items mail)",
|
|
553
|
-
input:
|
|
712
|
+
input: sendInput,
|
|
554
713
|
output: z.object({ accepted: z.boolean() }),
|
|
555
714
|
run: async (ctx, i) => {
|
|
556
|
-
await ctx.http.post(
|
|
715
|
+
await ctx.http.post(`${mailboxRoot(i.mailbox)}/sendMail`, {
|
|
716
|
+
message: messagePayload(i),
|
|
717
|
+
...(i.saveToSentItems !== undefined ? { saveToSentItems: i.saveToSentItems } : {}),
|
|
718
|
+
});
|
|
557
719
|
return { accepted: true };
|
|
558
720
|
},
|
|
559
721
|
}),
|
|
560
722
|
reply: action({
|
|
561
723
|
describe: "Send a reply to an existing message immediately (prefer drafts.createReply for human review)",
|
|
562
|
-
input:
|
|
724
|
+
input: mailboxInput.extend({
|
|
563
725
|
id: z.string().min(1),
|
|
564
726
|
comment: z.string().optional(),
|
|
565
727
|
to: recipientsInput.optional(),
|
|
566
728
|
}),
|
|
567
729
|
output: z.object({ accepted: z.boolean() }),
|
|
568
730
|
run: async (ctx, i) => {
|
|
569
|
-
await ctx.http.post(`${messagePath(i.id)}/reply`, {
|
|
731
|
+
await ctx.http.post(`${messagePath(i.id, i.mailbox)}/reply`, {
|
|
570
732
|
...(i.comment !== undefined ? { comment: i.comment } : {}),
|
|
571
733
|
...(i.to !== undefined ? { message: { toRecipients: recipientObjects(i.to) } } : {}),
|
|
572
734
|
});
|
|
@@ -575,37 +737,68 @@ export default defineConnector({
|
|
|
575
737
|
}),
|
|
576
738
|
markRead: action({
|
|
577
739
|
describe: "Mark a message read (sets isRead=true)",
|
|
578
|
-
input:
|
|
740
|
+
input: mailboxInput.extend({ id: z.string().min(1) }),
|
|
579
741
|
output: messageSummaryShape,
|
|
580
742
|
retrySafe: true,
|
|
581
|
-
run: async (ctx, i) => mapSummary((await ctx.http.patch(messagePath(i.id), { isRead: true })) as RawMessage),
|
|
743
|
+
run: async (ctx, i) => mapSummary((await ctx.http.patch(messagePath(i.id, i.mailbox), { isRead: true })) as RawMessage),
|
|
744
|
+
}),
|
|
745
|
+
setCategories: action({
|
|
746
|
+
describe: "Replace a message's Outlook categories",
|
|
747
|
+
input: mailboxInput.extend({ id: z.string().min(1), categories: z.array(z.string()) }),
|
|
748
|
+
output: messageSummaryShape,
|
|
749
|
+
retrySafe: true,
|
|
750
|
+
run: async (ctx, i) =>
|
|
751
|
+
mapSummary((await ctx.http.patch(messagePath(i.id, i.mailbox), { categories: i.categories })) as RawMessage),
|
|
752
|
+
}),
|
|
753
|
+
addCategories: action({
|
|
754
|
+
describe: "Add Outlook categories to a message without removing existing categories",
|
|
755
|
+
input: mailboxInput.extend({ id: z.string().min(1), categories: z.array(z.string()).min(1) }),
|
|
756
|
+
output: messageSummaryShape,
|
|
757
|
+
retrySafe: true,
|
|
758
|
+
run: async (ctx, i) => {
|
|
759
|
+
const current = (await ctx.http.get(messagePath(i.id, i.mailbox), { query: { $select: "id,categories" } })) as RawMessage;
|
|
760
|
+
const next = [...new Set([...(current.categories ?? []), ...i.categories])];
|
|
761
|
+
return mapSummary((await ctx.http.patch(messagePath(i.id, i.mailbox), { categories: next })) as RawMessage);
|
|
762
|
+
},
|
|
763
|
+
}),
|
|
764
|
+
removeCategories: action({
|
|
765
|
+
describe: "Remove Outlook categories from a message",
|
|
766
|
+
input: mailboxInput.extend({ id: z.string().min(1), categories: z.array(z.string()).min(1) }),
|
|
767
|
+
output: messageSummaryShape,
|
|
768
|
+
retrySafe: true,
|
|
769
|
+
run: async (ctx, i) => {
|
|
770
|
+
const remove = new Set(i.categories);
|
|
771
|
+
const current = (await ctx.http.get(messagePath(i.id, i.mailbox), { query: { $select: "id,categories" } })) as RawMessage;
|
|
772
|
+
const next = (current.categories ?? []).filter((category) => !remove.has(category));
|
|
773
|
+
return mapSummary((await ctx.http.patch(messagePath(i.id, i.mailbox), { categories: next })) as RawMessage);
|
|
774
|
+
},
|
|
582
775
|
}),
|
|
583
776
|
move: action({
|
|
584
777
|
describe: "Move a message to another folder or well-known folder name (archive, deleteditems, junkemail, etc.)",
|
|
585
|
-
input:
|
|
778
|
+
input: mailboxInput.extend({ id: z.string().min(1), destinationId: z.string().min(1) }),
|
|
586
779
|
output: messageSummaryShape,
|
|
587
780
|
run: async (ctx, i) =>
|
|
588
|
-
mapSummary((await ctx.http.post(`${messagePath(i.id)}/move`, { destinationId: i.destinationId })) as RawMessage),
|
|
781
|
+
mapSummary((await ctx.http.post(`${messagePath(i.id, i.mailbox)}/move`, { destinationId: i.destinationId })) as RawMessage),
|
|
589
782
|
}),
|
|
590
783
|
attachments: {
|
|
591
784
|
list: action({
|
|
592
785
|
describe: "List a message's attachments",
|
|
593
|
-
input:
|
|
786
|
+
input: mailboxInput.extend({ messageId: z.string().min(1) }),
|
|
594
787
|
output: z.array(attachmentShape),
|
|
595
788
|
safety: "read",
|
|
596
789
|
run: async (ctx, i) => {
|
|
597
|
-
const raw = (await ctx.http.get(`${messagePath(i.messageId)}/attachments`)) as GraphCollection<RawAttachment>;
|
|
790
|
+
const raw = (await ctx.http.get(`${messagePath(i.messageId, i.mailbox)}/attachments`)) as GraphCollection<RawAttachment>;
|
|
598
791
|
return (raw.value ?? []).map(mapAttachment);
|
|
599
792
|
},
|
|
600
793
|
}),
|
|
601
794
|
get: action({
|
|
602
795
|
describe: "Fetch one file attachment including base64 contentBytes when Graph returns them",
|
|
603
|
-
input:
|
|
796
|
+
input: mailboxInput.extend({ messageId: z.string().min(1), id: z.string().min(1) }),
|
|
604
797
|
output: attachmentDetailShape,
|
|
605
798
|
safety: "read",
|
|
606
799
|
run: async (ctx, i) =>
|
|
607
800
|
mapAttachmentDetail(
|
|
608
|
-
(await ctx.http.get(`${messagePath(i.messageId)}/attachments/${encodeURIComponent(i.id)}`)) as RawAttachment,
|
|
801
|
+
(await ctx.http.get(`${messagePath(i.messageId, i.mailbox)}/attachments/${encodeURIComponent(i.id)}`)) as RawAttachment,
|
|
609
802
|
),
|
|
610
803
|
}),
|
|
611
804
|
},
|
|
@@ -613,13 +806,13 @@ export default defineConnector({
|
|
|
613
806
|
drafts: {
|
|
614
807
|
create: action({
|
|
615
808
|
describe: "Create a new draft message for human review",
|
|
616
|
-
input:
|
|
809
|
+
input: composeInput,
|
|
617
810
|
output: messageDetailShape,
|
|
618
|
-
run: async (ctx, i) => mapDetail((await ctx.http.post(
|
|
811
|
+
run: async (ctx, i) => mapDetail((await ctx.http.post(`${mailboxRoot(i.mailbox)}/messages`, messagePayload(i))) as RawMessage),
|
|
619
812
|
}),
|
|
620
813
|
createReply: action({
|
|
621
814
|
describe: "Create a reply draft for an existing message; send later with drafts.send",
|
|
622
|
-
input:
|
|
815
|
+
input: mailboxInput.extend({
|
|
623
816
|
messageId: z.string().min(1),
|
|
624
817
|
comment: z.string().optional(),
|
|
625
818
|
text: z.string().optional(),
|
|
@@ -636,7 +829,7 @@ export default defineConnector({
|
|
|
636
829
|
: i.comment !== undefined
|
|
637
830
|
? { comment: i.comment }
|
|
638
831
|
: undefined;
|
|
639
|
-
return mapDetail((await ctx.http.post(`${messagePath(i.messageId)}/createReply`, body)) as RawMessage);
|
|
832
|
+
return mapDetail((await ctx.http.post(`${messagePath(i.messageId, i.mailbox)}/createReply`, body)) as RawMessage);
|
|
640
833
|
},
|
|
641
834
|
}),
|
|
642
835
|
update: action({
|
|
@@ -644,14 +837,14 @@ export default defineConnector({
|
|
|
644
837
|
input: updateDraftInput,
|
|
645
838
|
output: messageDetailShape,
|
|
646
839
|
retrySafe: true,
|
|
647
|
-
run: async (ctx, i) => mapDetail((await ctx.http.patch(messagePath(i.id), draftPatch(i))) as RawMessage),
|
|
840
|
+
run: async (ctx, i) => mapDetail((await ctx.http.patch(messagePath(i.id, i.mailbox), draftPatch(i))) as RawMessage),
|
|
648
841
|
}),
|
|
649
842
|
send: action({
|
|
650
843
|
describe: "Send an existing draft message (not idempotent)",
|
|
651
|
-
input:
|
|
844
|
+
input: mailboxInput.extend({ id: z.string().min(1) }),
|
|
652
845
|
output: z.object({ accepted: z.boolean() }),
|
|
653
846
|
run: async (ctx, i) => {
|
|
654
|
-
await ctx.http.post(`${messagePath(i.id)}/send`);
|
|
847
|
+
await ctx.http.post(`${messagePath(i.id, i.mailbox)}/send`);
|
|
655
848
|
return { accepted: true };
|
|
656
849
|
},
|
|
657
850
|
}),
|
|
@@ -659,13 +852,13 @@ export default defineConnector({
|
|
|
659
852
|
mailFolders: {
|
|
660
853
|
list: action({
|
|
661
854
|
describe: "List root mail folders (well-known folders like inbox, archive, deleteditems can be used directly)",
|
|
662
|
-
input:
|
|
855
|
+
input: mailboxInput.extend({ includeHiddenFolders: z.boolean().default(false), maxResults: z.number().int().min(1).max(250).default(100) }),
|
|
663
856
|
output: z.array(folderShape),
|
|
664
857
|
safety: "read",
|
|
665
858
|
run: async (ctx, i) => {
|
|
666
859
|
const raw = await listCollection<RawFolder>(
|
|
667
860
|
ctx,
|
|
668
|
-
|
|
861
|
+
`${mailboxRoot(i.mailbox)}/mailFolders`,
|
|
669
862
|
{ includeHiddenFolders: i.includeHiddenFolders, $top: Math.min(i.maxResults, 100) },
|
|
670
863
|
i.maxResults,
|
|
671
864
|
);
|
|
@@ -673,15 +866,34 @@ export default defineConnector({
|
|
|
673
866
|
},
|
|
674
867
|
}),
|
|
675
868
|
},
|
|
869
|
+
categories: {
|
|
870
|
+
list: action({
|
|
871
|
+
describe: "List the mailbox's Outlook master categories",
|
|
872
|
+
input: mailboxInput.extend({}),
|
|
873
|
+
output: z.array(categoryShape),
|
|
874
|
+
safety: "read",
|
|
875
|
+
run: async (ctx, i) => {
|
|
876
|
+
const raw = (await ctx.http.get(masterCategoriesPath(i.mailbox))) as GraphCollection<RawCategory>;
|
|
877
|
+
return (raw.value ?? []).map(mapCategory);
|
|
878
|
+
},
|
|
879
|
+
}),
|
|
880
|
+
create: action({
|
|
881
|
+
describe: "Create an Outlook master category",
|
|
882
|
+
input: mailboxInput.extend({ displayName: z.string().min(1), color: z.string().default("preset0") }),
|
|
883
|
+
output: categoryShape,
|
|
884
|
+
run: async (ctx, i) =>
|
|
885
|
+
mapCategory((await ctx.http.post(masterCategoriesPath(i.mailbox), { displayName: i.displayName, color: i.color })) as RawCategory),
|
|
886
|
+
}),
|
|
887
|
+
},
|
|
676
888
|
},
|
|
677
889
|
triggers: {
|
|
678
890
|
messageReceived: trigger.poll({
|
|
679
891
|
describe: "Fires for each new message created in an Outlook mail folder (delta-query poll)",
|
|
680
|
-
input:
|
|
892
|
+
input: mailboxInput.extend({ folderId: z.string().default("inbox"), maxPageSize: z.number().int().min(1).max(100).default(25) }),
|
|
681
893
|
output: messageSummaryShape,
|
|
682
894
|
interval: { default: "1m", floor: "30s" },
|
|
683
895
|
order: "newest-first",
|
|
684
|
-
poll: (ctx, params, cursor) => pollCreatedMessages(ctx, params.folderId, params.maxPageSize, cursor),
|
|
896
|
+
poll: (ctx, params, cursor) => pollCreatedMessages(ctx, params.mailbox, params.folderId, params.maxPageSize, cursor),
|
|
685
897
|
dedupeKey: (raw) => (raw as RawMessage).id,
|
|
686
898
|
map: (raw) => mapSummary(raw as RawMessage),
|
|
687
899
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devosurf/tesser-connectors",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "Tesser connector library — typed integrations consumed as ctx.connections.<name>.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"zod": "^4.4.3",
|
|
17
|
-
"@devosurf/tesser-sdk": "0.1.0-alpha.
|
|
17
|
+
"@devosurf/tesser-sdk": "0.1.0-alpha.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@devosurf/tesser-testing": "^0.1.0-alpha.
|
|
20
|
+
"@devosurf/tesser-testing": "^0.1.0-alpha.3"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"index.ts",
|