@devosurf/tesser-connectors 0.1.0-alpha.1 → 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 +3 -1
- package/catalog/index.ts +13 -0
- package/index.ts +1 -0
- package/manifest.json +162 -0
- package/outlook-mail/index.ts +901 -0
- package/package.json +3 -3
- package/providers/microsoft.ts +18 -0
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
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
|
-
> Status: **implemented.** P0 set: `github`, `slack`, `http`, `resend`, `gmail`, `google-calendar
|
|
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/catalog/index.ts
CHANGED
|
@@ -40,6 +40,19 @@ export const catalog: Record<string, ProviderFacts> = {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
|
+
"microsoft": {
|
|
44
|
+
"id": "microsoft",
|
|
45
|
+
"displayName": "Microsoft",
|
|
46
|
+
"baseUrl": "https://graph.microsoft.com/v1.0",
|
|
47
|
+
"docsUrl": "https://learn.microsoft.com/graph",
|
|
48
|
+
"oauth2": {
|
|
49
|
+
"authorizeUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
50
|
+
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
51
|
+
"clientAuth": "body",
|
|
52
|
+
"scopeSeparator": " ",
|
|
53
|
+
"pkce": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
43
56
|
"resend": {
|
|
44
57
|
"id": "resend",
|
|
45
58
|
"displayName": "Resend",
|
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { default as googleDocs } from "./google-docs/index.js";
|
|
|
10
10
|
export { default as googleDrive } from "./google-drive/index.js";
|
|
11
11
|
export { default as googleSheets } from "./google-sheets/index.js";
|
|
12
12
|
export { default as http } from "./http/index.js";
|
|
13
|
+
export { default as outlookMail } from "./outlook-mail/index.js";
|
|
13
14
|
export { default as pi } from "./pi/index.js";
|
|
14
15
|
export { default as resend } from "./resend/index.js";
|
|
15
16
|
export { default as slack } from "./slack/index.js";
|
package/manifest.json
CHANGED
|
@@ -599,6 +599,168 @@
|
|
|
599
599
|
"triggers": [],
|
|
600
600
|
"describe": "Plain HTTP requests to any endpoint"
|
|
601
601
|
},
|
|
602
|
+
{
|
|
603
|
+
"id": "outlook-mail",
|
|
604
|
+
"auth": {
|
|
605
|
+
"default": {
|
|
606
|
+
"kind": "oauth2",
|
|
607
|
+
"scopes": [
|
|
608
|
+
"offline_access",
|
|
609
|
+
"Mail.Read",
|
|
610
|
+
"Mail.ReadWrite",
|
|
611
|
+
"Mail.Send",
|
|
612
|
+
"Mail.Read.Shared",
|
|
613
|
+
"Mail.ReadWrite.Shared",
|
|
614
|
+
"Mail.Send.Shared",
|
|
615
|
+
"MailboxSettings.Read",
|
|
616
|
+
"MailboxSettings.ReadWrite"
|
|
617
|
+
],
|
|
618
|
+
"flow": "auth_code",
|
|
619
|
+
"provider": "microsoft"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
"actions": [
|
|
623
|
+
{
|
|
624
|
+
"path": "messages.list",
|
|
625
|
+
"safety": "read",
|
|
626
|
+
"retrySafe": true,
|
|
627
|
+
"describe": "List messages in a folder using Microsoft Graph's stable mapped shape"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"path": "messages.listConversation",
|
|
631
|
+
"safety": "read",
|
|
632
|
+
"retrySafe": true,
|
|
633
|
+
"describe": "List messages in the same Outlook conversation"
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"path": "messages.get",
|
|
637
|
+
"safety": "read",
|
|
638
|
+
"retrySafe": true,
|
|
639
|
+
"describe": "Fetch one message with body, recipients, categories, and thread ids"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
"path": "messages.send",
|
|
643
|
+
"safety": "write",
|
|
644
|
+
"retrySafe": false,
|
|
645
|
+
"describe": "Send a new email using Outlook Mail (not idempotent; creates Sent Items mail)"
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
"path": "messages.reply",
|
|
649
|
+
"safety": "write",
|
|
650
|
+
"retrySafe": false,
|
|
651
|
+
"describe": "Send a reply to an existing message immediately (prefer drafts.createReply for human review)"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"path": "messages.markRead",
|
|
655
|
+
"safety": "write",
|
|
656
|
+
"retrySafe": true,
|
|
657
|
+
"describe": "Mark a message read (sets isRead=true)"
|
|
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
|
+
},
|
|
677
|
+
{
|
|
678
|
+
"path": "messages.move",
|
|
679
|
+
"safety": "write",
|
|
680
|
+
"retrySafe": false,
|
|
681
|
+
"describe": "Move a message to another folder or well-known folder name (archive, deleteditems, junkemail, etc.)"
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
"path": "messages.attachments.list",
|
|
685
|
+
"safety": "read",
|
|
686
|
+
"retrySafe": true,
|
|
687
|
+
"describe": "List a message's attachments"
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
"path": "messages.attachments.get",
|
|
691
|
+
"safety": "read",
|
|
692
|
+
"retrySafe": true,
|
|
693
|
+
"describe": "Fetch one file attachment including base64 contentBytes when Graph returns them"
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"path": "drafts.create",
|
|
697
|
+
"safety": "write",
|
|
698
|
+
"retrySafe": false,
|
|
699
|
+
"describe": "Create a new draft message for human review"
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
"path": "drafts.createReply",
|
|
703
|
+
"safety": "write",
|
|
704
|
+
"retrySafe": false,
|
|
705
|
+
"describe": "Create a reply draft for an existing message; send later with drafts.send"
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
"path": "drafts.update",
|
|
709
|
+
"safety": "write",
|
|
710
|
+
"retrySafe": true,
|
|
711
|
+
"describe": "Update a draft message's subject/body/recipients/categories"
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
"path": "drafts.send",
|
|
715
|
+
"safety": "write",
|
|
716
|
+
"retrySafe": false,
|
|
717
|
+
"describe": "Send an existing draft message (not idempotent)"
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
"path": "mailFolders.list",
|
|
721
|
+
"safety": "read",
|
|
722
|
+
"retrySafe": true,
|
|
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"
|
|
736
|
+
}
|
|
737
|
+
],
|
|
738
|
+
"triggers": [
|
|
739
|
+
{
|
|
740
|
+
"id": "messageReceived",
|
|
741
|
+
"strategy": "poll",
|
|
742
|
+
"intervalDefault": "1m",
|
|
743
|
+
"intervalFloor": "30s",
|
|
744
|
+
"describe": "Fires for each new message created in an Outlook mail folder (delta-query poll)"
|
|
745
|
+
}
|
|
746
|
+
],
|
|
747
|
+
"describe": "Outlook Mail via Microsoft Graph — read, draft, send, label, and organize mail as the connected account",
|
|
748
|
+
"provider": "microsoft",
|
|
749
|
+
"providerFacts": {
|
|
750
|
+
"id": "microsoft",
|
|
751
|
+
"displayName": "Microsoft",
|
|
752
|
+
"baseUrl": "https://graph.microsoft.com/v1.0",
|
|
753
|
+
"docsUrl": "https://learn.microsoft.com/graph",
|
|
754
|
+
"oauth2": {
|
|
755
|
+
"authorizeUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
756
|
+
"tokenUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
757
|
+
"clientAuth": "body",
|
|
758
|
+
"scopeSeparator": " ",
|
|
759
|
+
"pkce": true
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
"baseUrl": "https://graph.microsoft.com/v1.0"
|
|
763
|
+
},
|
|
602
764
|
{
|
|
603
765
|
"id": "pi",
|
|
604
766
|
"auth": {
|
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
// Outlook Mail connector — Microsoft Graph mail surface for support-agent style
|
|
2
|
+
// inbox triage. Microsoft Graph sits behind the shared `microsoft` Provider so
|
|
3
|
+
// Teams/Excel/OneDrive can reuse the same OAuth app and token family later.
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { RetryableError, TerminalError } from "@devosurf/tesser-sdk";
|
|
7
|
+
import { action, defineConnector, oauth2, trigger, type ActionCtx } from "@devosurf/tesser-sdk/connector";
|
|
8
|
+
import { microsoftProvider } from "../providers/microsoft.js";
|
|
9
|
+
|
|
10
|
+
const SUMMARY_SELECT = [
|
|
11
|
+
"id",
|
|
12
|
+
"conversationId",
|
|
13
|
+
"subject",
|
|
14
|
+
"bodyPreview",
|
|
15
|
+
"receivedDateTime",
|
|
16
|
+
"webLink",
|
|
17
|
+
"isRead",
|
|
18
|
+
"hasAttachments",
|
|
19
|
+
"importance",
|
|
20
|
+
"categories",
|
|
21
|
+
"from",
|
|
22
|
+
"sender",
|
|
23
|
+
"parentFolderId",
|
|
24
|
+
].join(",");
|
|
25
|
+
|
|
26
|
+
const DETAIL_SELECT = [
|
|
27
|
+
SUMMARY_SELECT,
|
|
28
|
+
"body",
|
|
29
|
+
"toRecipients",
|
|
30
|
+
"ccRecipients",
|
|
31
|
+
"bccRecipients",
|
|
32
|
+
"replyTo",
|
|
33
|
+
"internetMessageId",
|
|
34
|
+
"isDraft",
|
|
35
|
+
].join(",");
|
|
36
|
+
|
|
37
|
+
const addressShape = z.object({ name: z.string(), address: z.string() });
|
|
38
|
+
const messageSummaryShape = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
conversationId: z.string(),
|
|
41
|
+
parentFolderId: z.string(),
|
|
42
|
+
from: z.string(),
|
|
43
|
+
fromName: z.string(),
|
|
44
|
+
subject: z.string(),
|
|
45
|
+
bodyPreview: z.string(),
|
|
46
|
+
receivedAt: z.string(),
|
|
47
|
+
webLink: z.string().optional(),
|
|
48
|
+
isRead: z.boolean(),
|
|
49
|
+
hasAttachments: z.boolean(),
|
|
50
|
+
categories: z.array(z.string()),
|
|
51
|
+
importance: z.string(),
|
|
52
|
+
});
|
|
53
|
+
const messageDetailShape = messageSummaryShape.extend({
|
|
54
|
+
body: z.string(),
|
|
55
|
+
bodyContentType: z.enum(["text", "html", "unknown"]),
|
|
56
|
+
to: z.array(addressShape),
|
|
57
|
+
cc: z.array(addressShape),
|
|
58
|
+
bcc: z.array(addressShape),
|
|
59
|
+
replyTo: z.array(addressShape),
|
|
60
|
+
internetMessageId: z.string().optional(),
|
|
61
|
+
isDraft: z.boolean(),
|
|
62
|
+
});
|
|
63
|
+
const folderShape = z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
displayName: z.string(),
|
|
66
|
+
parentFolderId: z.string(),
|
|
67
|
+
childFolderCount: z.number(),
|
|
68
|
+
unreadItemCount: z.number(),
|
|
69
|
+
totalItemCount: z.number(),
|
|
70
|
+
isHidden: z.boolean(),
|
|
71
|
+
});
|
|
72
|
+
const attachmentShape = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
name: z.string(),
|
|
75
|
+
contentType: z.string(),
|
|
76
|
+
size: z.number(),
|
|
77
|
+
isInline: z.boolean(),
|
|
78
|
+
});
|
|
79
|
+
const attachmentDetailShape = attachmentShape.extend({ contentBytes: z.string() });
|
|
80
|
+
const categoryShape = z.object({ id: z.string(), displayName: z.string(), color: z.string() });
|
|
81
|
+
|
|
82
|
+
const mailboxInput = z.object({ mailbox: z.string().min(1).optional() });
|
|
83
|
+
const recipientsInput = z.union([z.string().min(3), z.array(z.string().min(3)).min(1)]);
|
|
84
|
+
const bodyContentTypeInput = z.enum(["text", "html"]).default("text");
|
|
85
|
+
|
|
86
|
+
const composeInput = mailboxInput.extend({
|
|
87
|
+
to: recipientsInput,
|
|
88
|
+
subject: z.string(),
|
|
89
|
+
text: z.string(),
|
|
90
|
+
html: z.boolean().optional(),
|
|
91
|
+
cc: recipientsInput.optional(),
|
|
92
|
+
bcc: recipientsInput.optional(),
|
|
93
|
+
replyTo: recipientsInput.optional(),
|
|
94
|
+
from: z.string().min(3).optional(),
|
|
95
|
+
categories: z.array(z.string()).optional(),
|
|
96
|
+
importance: z.enum(["low", "normal", "high"]).optional(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const sendInput = composeInput.extend({ saveToSentItems: z.boolean().optional() });
|
|
100
|
+
|
|
101
|
+
const updateDraftInput = mailboxInput.extend({
|
|
102
|
+
id: z.string().min(1),
|
|
103
|
+
to: recipientsInput.optional(),
|
|
104
|
+
subject: z.string().optional(),
|
|
105
|
+
text: z.string().optional(),
|
|
106
|
+
html: z.boolean().optional(),
|
|
107
|
+
cc: recipientsInput.optional(),
|
|
108
|
+
bcc: recipientsInput.optional(),
|
|
109
|
+
replyTo: recipientsInput.optional(),
|
|
110
|
+
from: z.string().min(3).optional(),
|
|
111
|
+
categories: z.array(z.string()).optional(),
|
|
112
|
+
importance: z.enum(["low", "normal", "high"]).optional(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
type RawEmailAddress = { name?: string; address?: string };
|
|
116
|
+
type RawRecipient = { emailAddress?: RawEmailAddress };
|
|
117
|
+
type RawMessage = {
|
|
118
|
+
id: string;
|
|
119
|
+
conversationId?: string;
|
|
120
|
+
parentFolderId?: string;
|
|
121
|
+
from?: RawRecipient;
|
|
122
|
+
sender?: RawRecipient;
|
|
123
|
+
subject?: string;
|
|
124
|
+
bodyPreview?: string;
|
|
125
|
+
receivedDateTime?: string;
|
|
126
|
+
webLink?: string;
|
|
127
|
+
isRead?: boolean;
|
|
128
|
+
hasAttachments?: boolean;
|
|
129
|
+
categories?: string[];
|
|
130
|
+
importance?: string;
|
|
131
|
+
body?: { contentType?: string; content?: string };
|
|
132
|
+
toRecipients?: RawRecipient[];
|
|
133
|
+
ccRecipients?: RawRecipient[];
|
|
134
|
+
bccRecipients?: RawRecipient[];
|
|
135
|
+
replyTo?: RawRecipient[];
|
|
136
|
+
internetMessageId?: string;
|
|
137
|
+
isDraft?: boolean;
|
|
138
|
+
"@removed"?: { reason?: string };
|
|
139
|
+
};
|
|
140
|
+
type RawFolder = {
|
|
141
|
+
id: string;
|
|
142
|
+
displayName?: string;
|
|
143
|
+
parentFolderId?: string;
|
|
144
|
+
childFolderCount?: number;
|
|
145
|
+
unreadItemCount?: number;
|
|
146
|
+
totalItemCount?: number;
|
|
147
|
+
isHidden?: boolean;
|
|
148
|
+
};
|
|
149
|
+
type RawAttachment = {
|
|
150
|
+
id: string;
|
|
151
|
+
name?: string;
|
|
152
|
+
contentType?: string;
|
|
153
|
+
size?: number;
|
|
154
|
+
isInline?: boolean;
|
|
155
|
+
contentBytes?: string;
|
|
156
|
+
};
|
|
157
|
+
type RawCategory = { id: string; displayName?: string; color?: string };
|
|
158
|
+
type DeltaCursor = { mailbox?: string; folderId: string; link: string; seeded: boolean };
|
|
159
|
+
type GraphCollection<T> = {
|
|
160
|
+
value?: T[];
|
|
161
|
+
"@odata.nextLink"?: string;
|
|
162
|
+
"@odata.deltaLink"?: string;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function address(raw: RawRecipient | undefined): z.infer<typeof addressShape> {
|
|
166
|
+
return {
|
|
167
|
+
name: raw?.emailAddress?.name ?? "",
|
|
168
|
+
address: raw?.emailAddress?.address ?? "",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addresses(raw: RawRecipient[] | undefined): Array<z.infer<typeof addressShape>> {
|
|
173
|
+
return (raw ?? []).map(address);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function mapSummary(raw: RawMessage): z.infer<typeof messageSummaryShape> {
|
|
177
|
+
const from = address(raw.from ?? raw.sender);
|
|
178
|
+
return {
|
|
179
|
+
id: raw.id,
|
|
180
|
+
conversationId: raw.conversationId ?? "",
|
|
181
|
+
parentFolderId: raw.parentFolderId ?? "",
|
|
182
|
+
from: from.address,
|
|
183
|
+
fromName: from.name,
|
|
184
|
+
subject: raw.subject ?? "",
|
|
185
|
+
bodyPreview: raw.bodyPreview ?? "",
|
|
186
|
+
receivedAt: raw.receivedDateTime ?? "",
|
|
187
|
+
...(raw.webLink !== undefined ? { webLink: raw.webLink } : {}),
|
|
188
|
+
isRead: raw.isRead ?? false,
|
|
189
|
+
hasAttachments: raw.hasAttachments ?? false,
|
|
190
|
+
categories: raw.categories ?? [],
|
|
191
|
+
importance: raw.importance ?? "normal",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function mapDetail(raw: RawMessage): z.infer<typeof messageDetailShape> {
|
|
196
|
+
const contentType = (raw.body?.contentType ?? "unknown").toLowerCase();
|
|
197
|
+
return {
|
|
198
|
+
...mapSummary(raw),
|
|
199
|
+
body: raw.body?.content ?? "",
|
|
200
|
+
bodyContentType: contentType === "text" || contentType === "html" ? contentType : "unknown",
|
|
201
|
+
to: addresses(raw.toRecipients),
|
|
202
|
+
cc: addresses(raw.ccRecipients),
|
|
203
|
+
bcc: addresses(raw.bccRecipients),
|
|
204
|
+
replyTo: addresses(raw.replyTo),
|
|
205
|
+
...(raw.internetMessageId !== undefined ? { internetMessageId: raw.internetMessageId } : {}),
|
|
206
|
+
isDraft: raw.isDraft ?? false,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function mapFolder(raw: RawFolder): z.infer<typeof folderShape> {
|
|
211
|
+
return {
|
|
212
|
+
id: raw.id,
|
|
213
|
+
displayName: raw.displayName ?? "",
|
|
214
|
+
parentFolderId: raw.parentFolderId ?? "",
|
|
215
|
+
childFolderCount: raw.childFolderCount ?? 0,
|
|
216
|
+
unreadItemCount: raw.unreadItemCount ?? 0,
|
|
217
|
+
totalItemCount: raw.totalItemCount ?? 0,
|
|
218
|
+
isHidden: raw.isHidden ?? false,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function mapAttachment(raw: RawAttachment): z.infer<typeof attachmentShape> {
|
|
223
|
+
return {
|
|
224
|
+
id: raw.id,
|
|
225
|
+
name: raw.name ?? "",
|
|
226
|
+
contentType: raw.contentType ?? "",
|
|
227
|
+
size: raw.size ?? 0,
|
|
228
|
+
isInline: raw.isInline ?? false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function mapAttachmentDetail(raw: RawAttachment): z.infer<typeof attachmentDetailShape> {
|
|
233
|
+
return { ...mapAttachment(raw), contentBytes: raw.contentBytes ?? "" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function mapCategory(raw: RawCategory): z.infer<typeof categoryShape> {
|
|
237
|
+
return { id: raw.id, displayName: raw.displayName ?? "", color: raw.color ?? "" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function recipientObjects(input: z.infer<typeof recipientsInput> | undefined): RawRecipient[] | undefined {
|
|
241
|
+
if (input === undefined) return undefined;
|
|
242
|
+
const values = Array.isArray(input) ? input : [input];
|
|
243
|
+
return values.map((address) => ({ emailAddress: { address } }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function messagePayload(input: z.infer<typeof composeInput>): Record<string, unknown> {
|
|
247
|
+
return {
|
|
248
|
+
subject: input.subject,
|
|
249
|
+
body: { contentType: input.html === true ? "HTML" : "Text", content: input.text },
|
|
250
|
+
toRecipients: recipientObjects(input.to),
|
|
251
|
+
...(input.cc !== undefined ? { ccRecipients: recipientObjects(input.cc) } : {}),
|
|
252
|
+
...(input.bcc !== undefined ? { bccRecipients: recipientObjects(input.bcc) } : {}),
|
|
253
|
+
...(input.replyTo !== undefined ? { replyTo: recipientObjects(input.replyTo) } : {}),
|
|
254
|
+
...(input.from !== undefined ? { from: { emailAddress: { address: input.from } } } : {}),
|
|
255
|
+
...(input.categories !== undefined ? { categories: input.categories } : {}),
|
|
256
|
+
...(input.importance !== undefined ? { importance: input.importance } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function draftPatch(input: z.infer<typeof updateDraftInput>): Record<string, unknown> {
|
|
261
|
+
return {
|
|
262
|
+
...(input.subject !== undefined ? { subject: input.subject } : {}),
|
|
263
|
+
...(input.text !== undefined ? { body: { contentType: input.html === true ? "HTML" : "Text", content: input.text } } : {}),
|
|
264
|
+
...(input.to !== undefined ? { toRecipients: recipientObjects(input.to) } : {}),
|
|
265
|
+
...(input.cc !== undefined ? { ccRecipients: recipientObjects(input.cc) } : {}),
|
|
266
|
+
...(input.bcc !== undefined ? { bccRecipients: recipientObjects(input.bcc) } : {}),
|
|
267
|
+
...(input.replyTo !== undefined ? { replyTo: recipientObjects(input.replyTo) } : {}),
|
|
268
|
+
...(input.from !== undefined ? { from: { emailAddress: { address: input.from } } } : {}),
|
|
269
|
+
...(input.categories !== undefined ? { categories: input.categories } : {}),
|
|
270
|
+
...(input.importance !== undefined ? { importance: input.importance } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
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`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function masterCategoriesPath(mailbox?: string): string {
|
|
287
|
+
return `${mailboxRoot(mailbox)}/outlook/masterCategories`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function listCollection<T>(
|
|
291
|
+
ctx: ActionCtx,
|
|
292
|
+
firstPath: string,
|
|
293
|
+
query: Record<string, string | number | boolean | undefined>,
|
|
294
|
+
limit: number,
|
|
295
|
+
): Promise<T[]> {
|
|
296
|
+
const out: T[] = [];
|
|
297
|
+
let path = firstPath;
|
|
298
|
+
for (let page = 0; page < 50 && out.length < limit; page++) {
|
|
299
|
+
const res = (await ctx.http.get(path, page === 0 ? { query } : undefined)) as GraphCollection<T>;
|
|
300
|
+
out.push(...(res.value ?? []));
|
|
301
|
+
if (!res["@odata.nextLink"]) return out.slice(0, limit);
|
|
302
|
+
path = res["@odata.nextLink"];
|
|
303
|
+
}
|
|
304
|
+
if (out.length >= limit) return out.slice(0, limit);
|
|
305
|
+
throw new RetryableError(`microsoft graph pagination exceeded safety cap for ${firstPath}`);
|
|
306
|
+
}
|
|
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
|
+
|
|
344
|
+
async function pollCreatedMessages(
|
|
345
|
+
ctx: ActionCtx,
|
|
346
|
+
mailbox: string | undefined,
|
|
347
|
+
folderId: string,
|
|
348
|
+
maxPageSize: number,
|
|
349
|
+
cursor: unknown,
|
|
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
|
+
};
|
|
374
|
+
}
|
|
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 : [] };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function preferBody(contentType: "text" | "html", allowUnsafeHtml?: boolean): string {
|
|
385
|
+
const parts = [`outlook.body-content-type=\"${contentType}\"`];
|
|
386
|
+
if (allowUnsafeHtml === true) parts.push("outlook.allow-unsafe-html");
|
|
387
|
+
return parts.join(", ");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export default defineConnector({
|
|
391
|
+
id: "outlook-mail",
|
|
392
|
+
describe: "Outlook Mail via Microsoft Graph — read, draft, send, label, and organize mail as the connected account",
|
|
393
|
+
provider: microsoftProvider,
|
|
394
|
+
baseUrl: "https://graph.microsoft.com/v1.0",
|
|
395
|
+
auth: oauth2({
|
|
396
|
+
provider: "microsoft",
|
|
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
|
+
],
|
|
408
|
+
}),
|
|
409
|
+
defaultHeaders: {
|
|
410
|
+
accept: "application/json",
|
|
411
|
+
},
|
|
412
|
+
samples: {
|
|
413
|
+
"messages.list": [
|
|
414
|
+
{
|
|
415
|
+
id: "AAMkAD1",
|
|
416
|
+
conversationId: "AAQkAD1",
|
|
417
|
+
parentFolderId: "inbox",
|
|
418
|
+
from: "ada@example.com",
|
|
419
|
+
fromName: "Ada",
|
|
420
|
+
subject: "sample subject",
|
|
421
|
+
bodyPreview: "hello",
|
|
422
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
423
|
+
webLink: "https://outlook.office.com/mail/inbox/id/AAMkAD1",
|
|
424
|
+
isRead: false,
|
|
425
|
+
hasAttachments: false,
|
|
426
|
+
categories: [],
|
|
427
|
+
importance: "normal",
|
|
428
|
+
},
|
|
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
|
+
],
|
|
446
|
+
"messages.get": {
|
|
447
|
+
id: "AAMkAD1",
|
|
448
|
+
conversationId: "AAQkAD1",
|
|
449
|
+
parentFolderId: "inbox",
|
|
450
|
+
from: "ada@example.com",
|
|
451
|
+
fromName: "Ada",
|
|
452
|
+
subject: "sample subject",
|
|
453
|
+
bodyPreview: "hello",
|
|
454
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
455
|
+
isRead: false,
|
|
456
|
+
hasAttachments: false,
|
|
457
|
+
categories: [],
|
|
458
|
+
importance: "normal",
|
|
459
|
+
body: "hello there",
|
|
460
|
+
bodyContentType: "text",
|
|
461
|
+
to: [{ name: "Support", address: "support@example.com" }],
|
|
462
|
+
cc: [],
|
|
463
|
+
bcc: [],
|
|
464
|
+
replyTo: [],
|
|
465
|
+
internetMessageId: "<m1@example.com>",
|
|
466
|
+
isDraft: false,
|
|
467
|
+
},
|
|
468
|
+
"messages.send": { accepted: true },
|
|
469
|
+
"messages.reply": { accepted: true },
|
|
470
|
+
"messages.markRead": {
|
|
471
|
+
id: "AAMkAD1",
|
|
472
|
+
conversationId: "AAQkAD1",
|
|
473
|
+
parentFolderId: "inbox",
|
|
474
|
+
from: "ada@example.com",
|
|
475
|
+
fromName: "Ada",
|
|
476
|
+
subject: "sample subject",
|
|
477
|
+
bodyPreview: "hello",
|
|
478
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
479
|
+
isRead: true,
|
|
480
|
+
hasAttachments: false,
|
|
481
|
+
categories: [],
|
|
482
|
+
importance: "normal",
|
|
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
|
+
},
|
|
526
|
+
"messages.move": {
|
|
527
|
+
id: "AAMkAD2",
|
|
528
|
+
conversationId: "AAQkAD1",
|
|
529
|
+
parentFolderId: "archive",
|
|
530
|
+
from: "ada@example.com",
|
|
531
|
+
fromName: "Ada",
|
|
532
|
+
subject: "sample subject",
|
|
533
|
+
bodyPreview: "hello",
|
|
534
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
535
|
+
isRead: true,
|
|
536
|
+
hasAttachments: false,
|
|
537
|
+
categories: [],
|
|
538
|
+
importance: "normal",
|
|
539
|
+
},
|
|
540
|
+
"messages.attachments.list": [
|
|
541
|
+
{ id: "att1", name: "invoice.pdf", contentType: "application/pdf", size: 1234, isInline: false },
|
|
542
|
+
],
|
|
543
|
+
"messages.attachments.get": {
|
|
544
|
+
id: "att1",
|
|
545
|
+
name: "invoice.pdf",
|
|
546
|
+
contentType: "application/pdf",
|
|
547
|
+
size: 1234,
|
|
548
|
+
isInline: false,
|
|
549
|
+
contentBytes: "JVBERi0=",
|
|
550
|
+
},
|
|
551
|
+
"drafts.create": {
|
|
552
|
+
id: "draft1",
|
|
553
|
+
conversationId: "conv1",
|
|
554
|
+
parentFolderId: "drafts",
|
|
555
|
+
from: "support@example.com",
|
|
556
|
+
fromName: "Support",
|
|
557
|
+
subject: "Re: hello",
|
|
558
|
+
bodyPreview: "reply",
|
|
559
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
560
|
+
isRead: true,
|
|
561
|
+
hasAttachments: false,
|
|
562
|
+
categories: [],
|
|
563
|
+
importance: "normal",
|
|
564
|
+
body: "reply",
|
|
565
|
+
bodyContentType: "text",
|
|
566
|
+
to: [{ name: "Ada", address: "ada@example.com" }],
|
|
567
|
+
cc: [],
|
|
568
|
+
bcc: [],
|
|
569
|
+
replyTo: [],
|
|
570
|
+
isDraft: true,
|
|
571
|
+
},
|
|
572
|
+
"drafts.createReply": {
|
|
573
|
+
id: "replyDraft1",
|
|
574
|
+
conversationId: "conv1",
|
|
575
|
+
parentFolderId: "drafts",
|
|
576
|
+
from: "support@example.com",
|
|
577
|
+
fromName: "Support",
|
|
578
|
+
subject: "Re: hello",
|
|
579
|
+
bodyPreview: "reply",
|
|
580
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
581
|
+
isRead: true,
|
|
582
|
+
hasAttachments: false,
|
|
583
|
+
categories: [],
|
|
584
|
+
importance: "normal",
|
|
585
|
+
body: "reply",
|
|
586
|
+
bodyContentType: "text",
|
|
587
|
+
to: [{ name: "Ada", address: "ada@example.com" }],
|
|
588
|
+
cc: [],
|
|
589
|
+
bcc: [],
|
|
590
|
+
replyTo: [],
|
|
591
|
+
isDraft: true,
|
|
592
|
+
},
|
|
593
|
+
"drafts.update": {
|
|
594
|
+
id: "draft1",
|
|
595
|
+
conversationId: "conv1",
|
|
596
|
+
parentFolderId: "drafts",
|
|
597
|
+
from: "support@example.com",
|
|
598
|
+
fromName: "Support",
|
|
599
|
+
subject: "Re: hello",
|
|
600
|
+
bodyPreview: "updated",
|
|
601
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
602
|
+
isRead: true,
|
|
603
|
+
hasAttachments: false,
|
|
604
|
+
categories: [],
|
|
605
|
+
importance: "normal",
|
|
606
|
+
body: "updated",
|
|
607
|
+
bodyContentType: "text",
|
|
608
|
+
to: [{ name: "Ada", address: "ada@example.com" }],
|
|
609
|
+
cc: [],
|
|
610
|
+
bcc: [],
|
|
611
|
+
replyTo: [],
|
|
612
|
+
isDraft: true,
|
|
613
|
+
},
|
|
614
|
+
"drafts.send": { accepted: true },
|
|
615
|
+
"mailFolders.list": [
|
|
616
|
+
{
|
|
617
|
+
id: "inbox",
|
|
618
|
+
displayName: "Inbox",
|
|
619
|
+
parentFolderId: "root",
|
|
620
|
+
childFolderCount: 0,
|
|
621
|
+
unreadItemCount: 3,
|
|
622
|
+
totalItemCount: 10,
|
|
623
|
+
isHidden: false,
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
"categories.list": [{ id: "cat1", displayName: "Support", color: "preset0" }],
|
|
627
|
+
"categories.create": { id: "cat2", displayName: "Escalated", color: "preset1" },
|
|
628
|
+
"trigger:messageReceived": {
|
|
629
|
+
id: "AAMkAD3",
|
|
630
|
+
conversationId: "AAQkAD3",
|
|
631
|
+
parentFolderId: "inbox",
|
|
632
|
+
from: "ada@example.com",
|
|
633
|
+
fromName: "Ada",
|
|
634
|
+
subject: "new mail",
|
|
635
|
+
bodyPreview: "hello",
|
|
636
|
+
receivedAt: "2026-01-01T00:00:00Z",
|
|
637
|
+
isRead: false,
|
|
638
|
+
hasAttachments: false,
|
|
639
|
+
categories: [],
|
|
640
|
+
importance: "normal",
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
actions: {
|
|
644
|
+
messages: {
|
|
645
|
+
list: action({
|
|
646
|
+
describe: "List messages in a folder using Microsoft Graph's stable mapped shape",
|
|
647
|
+
input: mailboxInput.extend({
|
|
648
|
+
folderId: z.string().default("inbox"),
|
|
649
|
+
filter: z.string().optional(),
|
|
650
|
+
maxResults: z.number().int().min(1).max(250).default(25),
|
|
651
|
+
}),
|
|
652
|
+
output: z.array(messageSummaryShape),
|
|
653
|
+
safety: "read",
|
|
654
|
+
run: async (ctx, i) => {
|
|
655
|
+
const raw = await listCollection<RawMessage>(
|
|
656
|
+
ctx,
|
|
657
|
+
folderMessagesPath(i.folderId, i.mailbox),
|
|
658
|
+
{
|
|
659
|
+
$select: SUMMARY_SELECT,
|
|
660
|
+
$orderby: "receivedDateTime desc",
|
|
661
|
+
$top: Math.min(i.maxResults, 100),
|
|
662
|
+
...(i.filter !== undefined ? { $filter: i.filter } : {}),
|
|
663
|
+
},
|
|
664
|
+
i.maxResults,
|
|
665
|
+
);
|
|
666
|
+
return raw.map(mapSummary);
|
|
667
|
+
},
|
|
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
|
+
}),
|
|
693
|
+
get: action({
|
|
694
|
+
describe: "Fetch one message with body, recipients, categories, and thread ids",
|
|
695
|
+
input: mailboxInput.extend({
|
|
696
|
+
id: z.string().min(1),
|
|
697
|
+
bodyContentType: bodyContentTypeInput,
|
|
698
|
+
allowUnsafeHtml: z.boolean().optional(),
|
|
699
|
+
}),
|
|
700
|
+
output: messageDetailShape,
|
|
701
|
+
safety: "read",
|
|
702
|
+
run: async (ctx, i) => {
|
|
703
|
+
const raw = (await ctx.http.get(messagePath(i.id, i.mailbox), {
|
|
704
|
+
query: { $select: DETAIL_SELECT },
|
|
705
|
+
headers: { Prefer: preferBody(i.bodyContentType, i.allowUnsafeHtml) },
|
|
706
|
+
})) as RawMessage;
|
|
707
|
+
return mapDetail(raw);
|
|
708
|
+
},
|
|
709
|
+
}),
|
|
710
|
+
send: action({
|
|
711
|
+
describe: "Send a new email using Outlook Mail (not idempotent; creates Sent Items mail)",
|
|
712
|
+
input: sendInput,
|
|
713
|
+
output: z.object({ accepted: z.boolean() }),
|
|
714
|
+
run: async (ctx, i) => {
|
|
715
|
+
await ctx.http.post(`${mailboxRoot(i.mailbox)}/sendMail`, {
|
|
716
|
+
message: messagePayload(i),
|
|
717
|
+
...(i.saveToSentItems !== undefined ? { saveToSentItems: i.saveToSentItems } : {}),
|
|
718
|
+
});
|
|
719
|
+
return { accepted: true };
|
|
720
|
+
},
|
|
721
|
+
}),
|
|
722
|
+
reply: action({
|
|
723
|
+
describe: "Send a reply to an existing message immediately (prefer drafts.createReply for human review)",
|
|
724
|
+
input: mailboxInput.extend({
|
|
725
|
+
id: z.string().min(1),
|
|
726
|
+
comment: z.string().optional(),
|
|
727
|
+
to: recipientsInput.optional(),
|
|
728
|
+
}),
|
|
729
|
+
output: z.object({ accepted: z.boolean() }),
|
|
730
|
+
run: async (ctx, i) => {
|
|
731
|
+
await ctx.http.post(`${messagePath(i.id, i.mailbox)}/reply`, {
|
|
732
|
+
...(i.comment !== undefined ? { comment: i.comment } : {}),
|
|
733
|
+
...(i.to !== undefined ? { message: { toRecipients: recipientObjects(i.to) } } : {}),
|
|
734
|
+
});
|
|
735
|
+
return { accepted: true };
|
|
736
|
+
},
|
|
737
|
+
}),
|
|
738
|
+
markRead: action({
|
|
739
|
+
describe: "Mark a message read (sets isRead=true)",
|
|
740
|
+
input: mailboxInput.extend({ id: z.string().min(1) }),
|
|
741
|
+
output: messageSummaryShape,
|
|
742
|
+
retrySafe: true,
|
|
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
|
+
},
|
|
775
|
+
}),
|
|
776
|
+
move: action({
|
|
777
|
+
describe: "Move a message to another folder or well-known folder name (archive, deleteditems, junkemail, etc.)",
|
|
778
|
+
input: mailboxInput.extend({ id: z.string().min(1), destinationId: z.string().min(1) }),
|
|
779
|
+
output: messageSummaryShape,
|
|
780
|
+
run: async (ctx, i) =>
|
|
781
|
+
mapSummary((await ctx.http.post(`${messagePath(i.id, i.mailbox)}/move`, { destinationId: i.destinationId })) as RawMessage),
|
|
782
|
+
}),
|
|
783
|
+
attachments: {
|
|
784
|
+
list: action({
|
|
785
|
+
describe: "List a message's attachments",
|
|
786
|
+
input: mailboxInput.extend({ messageId: z.string().min(1) }),
|
|
787
|
+
output: z.array(attachmentShape),
|
|
788
|
+
safety: "read",
|
|
789
|
+
run: async (ctx, i) => {
|
|
790
|
+
const raw = (await ctx.http.get(`${messagePath(i.messageId, i.mailbox)}/attachments`)) as GraphCollection<RawAttachment>;
|
|
791
|
+
return (raw.value ?? []).map(mapAttachment);
|
|
792
|
+
},
|
|
793
|
+
}),
|
|
794
|
+
get: action({
|
|
795
|
+
describe: "Fetch one file attachment including base64 contentBytes when Graph returns them",
|
|
796
|
+
input: mailboxInput.extend({ messageId: z.string().min(1), id: z.string().min(1) }),
|
|
797
|
+
output: attachmentDetailShape,
|
|
798
|
+
safety: "read",
|
|
799
|
+
run: async (ctx, i) =>
|
|
800
|
+
mapAttachmentDetail(
|
|
801
|
+
(await ctx.http.get(`${messagePath(i.messageId, i.mailbox)}/attachments/${encodeURIComponent(i.id)}`)) as RawAttachment,
|
|
802
|
+
),
|
|
803
|
+
}),
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
drafts: {
|
|
807
|
+
create: action({
|
|
808
|
+
describe: "Create a new draft message for human review",
|
|
809
|
+
input: composeInput,
|
|
810
|
+
output: messageDetailShape,
|
|
811
|
+
run: async (ctx, i) => mapDetail((await ctx.http.post(`${mailboxRoot(i.mailbox)}/messages`, messagePayload(i))) as RawMessage),
|
|
812
|
+
}),
|
|
813
|
+
createReply: action({
|
|
814
|
+
describe: "Create a reply draft for an existing message; send later with drafts.send",
|
|
815
|
+
input: mailboxInput.extend({
|
|
816
|
+
messageId: z.string().min(1),
|
|
817
|
+
comment: z.string().optional(),
|
|
818
|
+
text: z.string().optional(),
|
|
819
|
+
html: z.boolean().optional(),
|
|
820
|
+
}),
|
|
821
|
+
output: messageDetailShape,
|
|
822
|
+
run: async (ctx, i) => {
|
|
823
|
+
if (i.comment !== undefined && i.text !== undefined) {
|
|
824
|
+
throw new TerminalError("outlook-mail.drafts.createReply: pass either comment or text, not both");
|
|
825
|
+
}
|
|
826
|
+
const body =
|
|
827
|
+
i.text !== undefined
|
|
828
|
+
? { message: { body: { contentType: i.html === true ? "HTML" : "Text", content: i.text } } }
|
|
829
|
+
: i.comment !== undefined
|
|
830
|
+
? { comment: i.comment }
|
|
831
|
+
: undefined;
|
|
832
|
+
return mapDetail((await ctx.http.post(`${messagePath(i.messageId, i.mailbox)}/createReply`, body)) as RawMessage);
|
|
833
|
+
},
|
|
834
|
+
}),
|
|
835
|
+
update: action({
|
|
836
|
+
describe: "Update a draft message's subject/body/recipients/categories",
|
|
837
|
+
input: updateDraftInput,
|
|
838
|
+
output: messageDetailShape,
|
|
839
|
+
retrySafe: true,
|
|
840
|
+
run: async (ctx, i) => mapDetail((await ctx.http.patch(messagePath(i.id, i.mailbox), draftPatch(i))) as RawMessage),
|
|
841
|
+
}),
|
|
842
|
+
send: action({
|
|
843
|
+
describe: "Send an existing draft message (not idempotent)",
|
|
844
|
+
input: mailboxInput.extend({ id: z.string().min(1) }),
|
|
845
|
+
output: z.object({ accepted: z.boolean() }),
|
|
846
|
+
run: async (ctx, i) => {
|
|
847
|
+
await ctx.http.post(`${messagePath(i.id, i.mailbox)}/send`);
|
|
848
|
+
return { accepted: true };
|
|
849
|
+
},
|
|
850
|
+
}),
|
|
851
|
+
},
|
|
852
|
+
mailFolders: {
|
|
853
|
+
list: action({
|
|
854
|
+
describe: "List root mail folders (well-known folders like inbox, archive, deleteditems can be used directly)",
|
|
855
|
+
input: mailboxInput.extend({ includeHiddenFolders: z.boolean().default(false), maxResults: z.number().int().min(1).max(250).default(100) }),
|
|
856
|
+
output: z.array(folderShape),
|
|
857
|
+
safety: "read",
|
|
858
|
+
run: async (ctx, i) => {
|
|
859
|
+
const raw = await listCollection<RawFolder>(
|
|
860
|
+
ctx,
|
|
861
|
+
`${mailboxRoot(i.mailbox)}/mailFolders`,
|
|
862
|
+
{ includeHiddenFolders: i.includeHiddenFolders, $top: Math.min(i.maxResults, 100) },
|
|
863
|
+
i.maxResults,
|
|
864
|
+
);
|
|
865
|
+
return raw.map(mapFolder);
|
|
866
|
+
},
|
|
867
|
+
}),
|
|
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
|
+
},
|
|
888
|
+
},
|
|
889
|
+
triggers: {
|
|
890
|
+
messageReceived: trigger.poll({
|
|
891
|
+
describe: "Fires for each new message created in an Outlook mail folder (delta-query poll)",
|
|
892
|
+
input: mailboxInput.extend({ folderId: z.string().default("inbox"), maxPageSize: z.number().int().min(1).max(100).default(25) }),
|
|
893
|
+
output: messageSummaryShape,
|
|
894
|
+
interval: { default: "1m", floor: "30s" },
|
|
895
|
+
order: "newest-first",
|
|
896
|
+
poll: (ctx, params, cursor) => pollCreatedMessages(ctx, params.mailbox, params.folderId, params.maxPageSize, cursor),
|
|
897
|
+
dedupeKey: (raw) => (raw as RawMessage).id,
|
|
898
|
+
map: (raw) => mapSummary(raw as RawMessage),
|
|
899
|
+
}),
|
|
900
|
+
},
|
|
901
|
+
});
|
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",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProviderFacts } from "@devosurf/tesser-sdk/connector";
|
|
2
|
+
|
|
3
|
+
// Shared Provider for Microsoft Graph surfaces (Outlook Mail, Teams, Excel,
|
|
4
|
+
// OneDrive/SharePoint). One Entra app backs the family; individual Connectors
|
|
5
|
+
// contribute their own Graph scopes.
|
|
6
|
+
export const microsoftProvider: ProviderFacts = {
|
|
7
|
+
id: "microsoft",
|
|
8
|
+
displayName: "Microsoft",
|
|
9
|
+
baseUrl: "https://graph.microsoft.com/v1.0",
|
|
10
|
+
docsUrl: "https://learn.microsoft.com/graph",
|
|
11
|
+
oauth2: {
|
|
12
|
+
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
13
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
14
|
+
clientAuth: "body",
|
|
15
|
+
scopeSeparator: " ",
|
|
16
|
+
pkce: true,
|
|
17
|
+
},
|
|
18
|
+
};
|