@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 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": [
@@ -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 sendOrDraftInput = z.object({
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 updateDraftInput = z.object({
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 sendOrDraftInput>): Record<string, unknown> {
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 messagePath(id: string): string {
261
- return `/me/messages/${encodeURIComponent(id)}`;
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 folderMessagesPath(folderId: string): string {
265
- return `/me/mailFolders/${encodeURIComponent(folderId)}/messages`;
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 < 20 && out.length < limit; 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?: string }> {
292
- const items: RawMessage[] = [];
293
- let path = typeof cursor === "string" && cursor.length > 0 ? cursor : `${folderMessagesPath(folderId)}/delta`;
294
- for (let page = 0; page < 20; page++) {
295
- const first = page === 0 && !(typeof cursor === "string" && cursor.length > 0);
296
- const res = (await ctx.http.get(path, {
297
- ...(first
298
- ? {
299
- query: {
300
- changeType: "created",
301
- $select: SUMMARY_SELECT,
302
- $orderby: "receivedDateTime desc",
303
- $top: maxPageSize,
304
- },
305
- }
306
- : {}),
307
- headers: { Prefer: `odata.maxpagesize=${maxPageSize}` },
308
- })) as GraphCollection<RawMessage>;
309
- items.push(...(res.value ?? []).filter((m) => m["@removed"] === undefined));
310
- if (res["@odata.deltaLink"] !== undefined) {
311
- return { items, nextCursor: res["@odata.deltaLink"] };
312
- }
313
- if (res["@odata.nextLink"] === undefined) return { items };
314
- path = res["@odata.nextLink"];
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
- throw new RetryableError(`microsoft graph delta pagination exceeded safety cap for ${folderMessagesPath(folderId)}`);
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: ["offline_access", "Mail.Read", "Mail.ReadWrite", "Mail.Send"],
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: z.object({
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: z.object({
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: sendOrDraftInput,
712
+ input: sendInput,
554
713
  output: z.object({ accepted: z.boolean() }),
555
714
  run: async (ctx, i) => {
556
- await ctx.http.post("/me/sendMail", { message: messagePayload(i) });
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: z.object({
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: z.object({ id: z.string().min(1) }),
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: z.object({ id: z.string().min(1), destinationId: z.string().min(1) }),
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: z.object({ messageId: z.string().min(1) }),
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: z.object({ messageId: z.string().min(1), id: z.string().min(1) }),
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: sendOrDraftInput,
809
+ input: composeInput,
617
810
  output: messageDetailShape,
618
- run: async (ctx, i) => mapDetail((await ctx.http.post("/me/messages", messagePayload(i))) as RawMessage),
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: z.object({
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: z.object({ id: z.string().min(1) }),
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: z.object({ includeHiddenFolders: z.boolean().default(false), maxResults: z.number().int().min(1).max(250).default(100) }),
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
- "/me/mailFolders",
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: z.object({ folderId: z.string().default("inbox"), maxPageSize: z.number().int().min(1).max(100).default(25) }),
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.2",
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.2"
17
+ "@devosurf/tesser-sdk": "0.1.0-alpha.3"
18
18
  },
19
19
  "devDependencies": {
20
- "@devosurf/tesser-testing": "^0.1.0-alpha.2"
20
+ "@devosurf/tesser-testing": "^0.1.0-alpha.3"
21
21
  },
22
22
  "files": [
23
23
  "index.ts",