runline 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/credentials/aws-plugin.d.ts +3 -0
- package/dist/credentials/aws-plugin.js +7 -2
- package/dist/credentials/aws.d.ts +11 -0
- package/dist/credentials/aws.js +110 -11
- package/dist/credentials/broker.js +8 -7
- package/dist/credentials/http.d.ts +17 -3
- package/dist/credentials/http.js +31 -9
- package/dist/credentials/transport.js +5 -1
- package/dist/credentials/types.d.ts +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/_shared/attachments.js +97 -0
- package/dist/plugins/_shared/microsoftAuth.js +33 -4
- package/dist/plugins/_shared/schemas.js +8 -0
- package/dist/plugins/agentMail/src/drafts.js +2 -1
- package/dist/plugins/agentMail/src/inboxes.js +2 -1
- package/dist/plugins/agentMail/src/messages.js +2 -1
- package/dist/plugins/agentMail/src/schemas.js +4 -41
- package/dist/plugins/agentMail/src/shared.js +28 -50
- package/dist/plugins/agentMail/src/threads.js +2 -1
- package/dist/plugins/gmail/src/index.js +10 -22
- package/dist/plugins/microsoftCalendar/src/index.js +513 -17
- package/dist/plugins/microsoftFiles/src/index.js +3 -3
- package/dist/plugins/microsoftMail/src/index.js +305 -54
- package/dist/plugins/todoist/src/credentials.js +4 -11
- package/dist/plugins/todoist/src/index.js +390 -281
- package/dist/plugins/zohoMail/src/credentials.js +92 -0
- package/dist/plugins/zohoMail/src/index.js +241 -0
- package/package.json +1 -1
|
@@ -1,27 +1,210 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Outlook mail plugin for runline (Microsoft Graph).
|
|
3
|
+
*
|
|
4
|
+
* Auth: Microsoft identity platform OAuth2 (delegated, acts as the signed-in
|
|
5
|
+
* user → /me) seeded by the OAuth flow; or app-only client credentials
|
|
6
|
+
* (set tenantId/clientId/clientSecret + userUpn) for an unattended service
|
|
7
|
+
* mailbox. Delegated consent belongs to the "microsoft" grant, so one Entra
|
|
8
|
+
* app and one cached token serve every Microsoft plugin. See
|
|
9
|
+
* _shared/microsoftAuth.ts.
|
|
10
|
+
*
|
|
11
|
+
* Graph delegated scopes: Mail.Send, Mail.ReadWrite, Mail.Read.
|
|
12
|
+
*
|
|
13
|
+
* Attachments follow Graph's two documented paths: a file under 3 MB goes
|
|
14
|
+
* in one request with its message (or one POST to a draft); a file of 3 to
|
|
15
|
+
* 150 MB is attached to a draft through an upload session, its bytes PUT
|
|
16
|
+
* in ranges under 4 MB to the session's pre-authenticated Outlook URL. A
|
|
17
|
+
* message built as a draft is sent once every file is attached, and the
|
|
18
|
+
* draft is deleted if any step fails.
|
|
19
|
+
*/
|
|
20
|
+
import { jsonAnswer, pathSegment, } from "runline";
|
|
21
|
+
import * as t from "typebox";
|
|
22
|
+
import { attachmentFile, attachmentSources, } from "../../_shared/attachments.js";
|
|
23
|
+
import { graphFailed, graphRequest, graphResponse, microsoftCredential, microsoftOAuth, outlookUpload, userBase, } from "../../_shared/microsoftAuth.js";
|
|
2
24
|
const NAME = "microsoftMail";
|
|
3
25
|
const SCOPES = [
|
|
4
26
|
"https://graph.microsoft.com/Mail.Send",
|
|
5
27
|
"https://graph.microsoft.com/Mail.ReadWrite",
|
|
6
28
|
"https://graph.microsoft.com/Mail.Read",
|
|
7
29
|
];
|
|
8
|
-
|
|
9
|
-
|
|
30
|
+
/** Graph attaches a file in one request only under 3 MB. */
|
|
31
|
+
const ONE_REQUEST_BYTES = 3 * 1024 * 1024;
|
|
32
|
+
/** Graph's ceiling for one file attachment, through an upload session. */
|
|
33
|
+
const MAX_ATTACHMENT_BYTES = 150 * 1024 * 1024;
|
|
34
|
+
/** Upload session ranges stay under Graph's 4 MB guidance. */
|
|
35
|
+
const UPLOAD_RANGE_BYTES = 3 * 1024 * 1024;
|
|
36
|
+
const STRICT = { additionalProperties: false };
|
|
37
|
+
const Id = (description) => t.String({ minLength: 1, ...(description ? { description } : {}) });
|
|
38
|
+
const Addresses = (description, minItems = 0) => t.Array(t.String({ minLength: 1 }), {
|
|
39
|
+
description,
|
|
40
|
+
...(minItems ? { minItems } : {}),
|
|
41
|
+
});
|
|
42
|
+
const Attachments = t.Array(t.Union([
|
|
43
|
+
...attachmentSources("Up to 150 MB a file; Exchange limits a whole message to 35 MB unless the tenant raises it."),
|
|
44
|
+
]), { minItems: 1 });
|
|
45
|
+
/** What mail.send and mail.draft compose a message from. */
|
|
46
|
+
const compose = {
|
|
47
|
+
to: t.Optional(Addresses("Recipient addresses")),
|
|
48
|
+
cc: t.Optional(Addresses("Cc addresses")),
|
|
49
|
+
bcc: t.Optional(Addresses("Bcc addresses")),
|
|
50
|
+
replyTo: t.Optional(Addresses("Reply-To addresses")),
|
|
51
|
+
subject: t.String(),
|
|
52
|
+
body: t.String(),
|
|
53
|
+
html: t.Optional(t.Boolean({ description: "body is HTML (default plain text)" })),
|
|
54
|
+
importance: t.Optional(t.Union([t.Literal("low"), t.Literal("normal"), t.Literal("high")])),
|
|
55
|
+
attachments: t.Optional(Attachments),
|
|
56
|
+
};
|
|
57
|
+
const send = t.Object({ ...compose, to: Addresses("Recipient addresses", 1) }, STRICT);
|
|
58
|
+
const draft = t.Object(compose, STRICT);
|
|
59
|
+
const byId = t.Object({ id: Id("Message id") }, STRICT);
|
|
60
|
+
const SendNow = t.Optional(t.Boolean({
|
|
61
|
+
description: "Send at once (default); false keeps the draft and returns it",
|
|
10
62
|
}));
|
|
11
|
-
|
|
63
|
+
const reply = t.Object({
|
|
64
|
+
id: Id("The message to reply to"),
|
|
65
|
+
comment: t.Optional(t.String({ description: "Text of the reply" })),
|
|
66
|
+
replyAll: t.Optional(t.Boolean({ description: "Reply to the sender and every recipient" })),
|
|
67
|
+
attachments: t.Optional(Attachments),
|
|
68
|
+
send: SendNow,
|
|
69
|
+
}, STRICT);
|
|
70
|
+
const forward = t.Object({
|
|
71
|
+
id: Id("The message to forward"),
|
|
72
|
+
to: Addresses("Recipient addresses", 1),
|
|
73
|
+
comment: t.Optional(t.String({ description: "Text above the message" })),
|
|
74
|
+
attachments: t.Optional(Attachments),
|
|
75
|
+
send: SendNow,
|
|
76
|
+
}, STRICT);
|
|
77
|
+
const list = t.Object({
|
|
78
|
+
search: t.Optional(Id("KQL search across the mailbox")),
|
|
79
|
+
top: t.Optional(t.Integer({ minimum: 1, description: "Default 20" })),
|
|
80
|
+
}, STRICT);
|
|
81
|
+
const attachmentAdd = t.Object({
|
|
82
|
+
messageId: Id("A draft message"),
|
|
83
|
+
attachment: t.Union([
|
|
84
|
+
...attachmentSources("Up to 150 MB; Exchange limits a whole message to 35 MB unless the tenant raises it."),
|
|
85
|
+
]),
|
|
86
|
+
}, STRICT);
|
|
87
|
+
const attachmentList = t.Object({ messageId: Id() }, STRICT);
|
|
88
|
+
const attachmentRef = t.Object({ messageId: Id(), attachmentId: Id("From attachment.list") }, STRICT);
|
|
89
|
+
const recipients = (addresses) => (addresses ?? []).map((address) => ({ emailAddress: { address } }));
|
|
90
|
+
function graph(ctx, method, path, body) {
|
|
91
|
+
return graphRequest(ctx, NAME, SCOPES, method, path, body);
|
|
92
|
+
}
|
|
93
|
+
const messagePath = (ctx, id) => `${userBase(ctx)}/messages/${pathSegment(id)}`;
|
|
94
|
+
/** Every attachment's bytes, named, before any request. */
|
|
95
|
+
function files(inputs = []) {
|
|
96
|
+
return Promise.all(inputs.map(async (input, index) => {
|
|
97
|
+
const file = await attachmentFile(input, index, {
|
|
98
|
+
plugin: NAME,
|
|
99
|
+
maxBytes: MAX_ATTACHMENT_BYTES,
|
|
100
|
+
oversize: `${NAME}: attachment ${index} exceeds Graph's 150 MB attachment limit`,
|
|
101
|
+
});
|
|
102
|
+
return { ...file, name: file.name ?? `attachment-${index + 1}`, input };
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/** Whether the files ride in the same request as their message. */
|
|
106
|
+
const oneRequest = (all) => all.reduce((sum, file) => sum + file.bytes.length, 0) < ONE_REQUEST_BYTES;
|
|
107
|
+
/** Graph's fileAttachment, for a file sent in one request. */
|
|
108
|
+
function fileAttachment(file) {
|
|
12
109
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
110
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
111
|
+
name: file.name,
|
|
112
|
+
contentBytes: file.bytes.toString("base64"),
|
|
113
|
+
...(file.mimeType ? { contentType: file.mimeType } : {}),
|
|
114
|
+
...(file.input.inline !== undefined ? { isInline: file.input.inline } : {}),
|
|
115
|
+
...(file.input.contentId ? { contentId: file.input.contentId } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** The attachment id in an upload session's final Location header. */
|
|
119
|
+
const LOCATION_ID = /Attachments\('([^']+)'\)/;
|
|
120
|
+
/**
|
|
121
|
+
* Attach one file of 3 to 150 MB through an upload session: its ranges
|
|
122
|
+
* PUT in order, each checked against the range the session expects next.
|
|
123
|
+
* A failed upload deletes the session.
|
|
124
|
+
*/
|
|
125
|
+
async function uploadSession(ctx, messageId, file) {
|
|
126
|
+
const session = await graph(ctx, "POST", `${messagePath(ctx, messageId)}/attachments/createUploadSession`, {
|
|
127
|
+
AttachmentItem: {
|
|
128
|
+
attachmentType: "file",
|
|
129
|
+
name: file.name,
|
|
130
|
+
size: file.bytes.length,
|
|
131
|
+
...(file.mimeType ? { contentType: file.mimeType } : {}),
|
|
132
|
+
...(file.input.inline !== undefined
|
|
133
|
+
? { isInline: file.input.inline }
|
|
134
|
+
: {}),
|
|
135
|
+
...(file.input.contentId ? { contentId: file.input.contentId } : {}),
|
|
17
136
|
},
|
|
18
|
-
|
|
19
|
-
|
|
137
|
+
});
|
|
138
|
+
const total = file.bytes.length;
|
|
139
|
+
try {
|
|
140
|
+
for (let start = 0; start < total; start += UPLOAD_RANGE_BYTES) {
|
|
141
|
+
const end = Math.min(start + UPLOAD_RANGE_BYTES, total);
|
|
142
|
+
const res = await outlookUpload(session.uploadUrl, {
|
|
143
|
+
method: "PUT",
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/octet-stream",
|
|
146
|
+
"Content-Range": `bytes ${start}-${end - 1}/${total}`,
|
|
147
|
+
},
|
|
148
|
+
body: file.bytes.subarray(start, end),
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok)
|
|
151
|
+
throw await graphFailed(NAME, res);
|
|
152
|
+
if (end === total)
|
|
153
|
+
return LOCATION_ID.exec(res.headers.get("location") ?? "")?.[1];
|
|
154
|
+
const { nextExpectedRanges } = await jsonAnswer(res);
|
|
155
|
+
const next = nextExpectedRanges?.[0]?.split("-")[0];
|
|
156
|
+
if (next !== undefined && Number(next) !== end)
|
|
157
|
+
throw new Error(`${NAME}: the upload session expects byte ${next}, not ${end}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
await outlookUpload(session.uploadUrl, { method: "DELETE" }).catch(() => undefined);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/** Attach one file to a draft, by the path its size takes. */
|
|
167
|
+
async function attach(ctx, messageId, file) {
|
|
168
|
+
const id = file.bytes.length < ONE_REQUEST_BYTES
|
|
169
|
+
? (await graph(ctx, "POST", `${messagePath(ctx, messageId)}/attachments`, fileAttachment(file))).id
|
|
170
|
+
: await uploadSession(ctx, messageId, file);
|
|
171
|
+
return { ...(id ? { id } : {}), name: file.name, size: file.bytes.length };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Finish a draft this call created: attach its files, then send it or
|
|
175
|
+
* hand it back. Any failure deletes the draft, so no half-built message
|
|
176
|
+
* is left in Drafts.
|
|
177
|
+
*/
|
|
178
|
+
async function finishDraft(ctx, created, pending, sendIt) {
|
|
179
|
+
try {
|
|
180
|
+
for (const file of pending)
|
|
181
|
+
await attach(ctx, created.id, file);
|
|
182
|
+
if (!sendIt)
|
|
183
|
+
return { id: created.id, webLink: created.webLink };
|
|
184
|
+
await graph(ctx, "POST", `${messagePath(ctx, created.id)}/send`);
|
|
185
|
+
return { success: true };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
await graph(ctx, "DELETE", messagePath(ctx, created.id)).catch(() => undefined);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** A Graph message from compose fields, with the files that ride along. */
|
|
193
|
+
function message(p, attached) {
|
|
194
|
+
return {
|
|
195
|
+
subject: p.subject,
|
|
196
|
+
body: { contentType: p.html ? "HTML" : "Text", content: p.body },
|
|
197
|
+
toRecipients: recipients(p.to),
|
|
198
|
+
ccRecipients: recipients(p.cc),
|
|
199
|
+
...(p.bcc ? { bccRecipients: recipients(p.bcc) } : {}),
|
|
200
|
+
...(p.replyTo ? { replyTo: recipients(p.replyTo) } : {}),
|
|
201
|
+
...(p.importance ? { importance: p.importance } : {}),
|
|
202
|
+
...(attached.length ? { attachments: attached.map(fileAttachment) } : {}),
|
|
20
203
|
};
|
|
21
204
|
}
|
|
22
205
|
export default function microsoftMail(rl) {
|
|
23
206
|
rl.setName(NAME);
|
|
24
|
-
rl.setVersion("1.
|
|
207
|
+
rl.setVersion("1.1.0");
|
|
25
208
|
rl.setCredential(microsoftCredential(NAME, SCOPES));
|
|
26
209
|
rl.setConnectionSchema({
|
|
27
210
|
authMethod: {
|
|
@@ -63,76 +246,144 @@ export default function microsoftMail(rl) {
|
|
|
63
246
|
rl.setOAuth(microsoftOAuth(SCOPES, "Mail.Send, Mail.ReadWrite, Mail.Read"));
|
|
64
247
|
rl.registerAction("mail.send", {
|
|
65
248
|
access: "write",
|
|
66
|
-
description: "Send an email as the connected mailbox. Returns {success}. Get user approval before sending external mail.",
|
|
67
|
-
inputSchema:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
249
|
+
description: "Send an email as the connected mailbox, with attachments. Returns {success}. Get user approval before sending external mail.",
|
|
250
|
+
inputSchema: send,
|
|
251
|
+
async execute(input, ctx) {
|
|
252
|
+
const p = input;
|
|
253
|
+
const all = await files(p.attachments);
|
|
254
|
+
if (oneRequest(all)) {
|
|
255
|
+
await graph(ctx, "POST", `${userBase(ctx)}/sendMail`, {
|
|
256
|
+
message: message(p, all),
|
|
257
|
+
saveToSentItems: true,
|
|
258
|
+
});
|
|
259
|
+
return { success: true };
|
|
260
|
+
}
|
|
261
|
+
const created = await graph(ctx, "POST", `${userBase(ctx)}/messages`, message(p, []));
|
|
262
|
+
return finishDraft(ctx, created, all, true);
|
|
81
263
|
},
|
|
264
|
+
});
|
|
265
|
+
rl.registerAction("mail.draft", {
|
|
266
|
+
access: "write",
|
|
267
|
+
description: "Create a draft email, with attachments (not sent). Returns {id, webLink}; draft.send sends it.",
|
|
268
|
+
inputSchema: draft,
|
|
82
269
|
async execute(input, ctx) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
270
|
+
const p = input;
|
|
271
|
+
const all = await files(p.attachments);
|
|
272
|
+
const together = oneRequest(all);
|
|
273
|
+
const created = await graph(ctx, "POST", `${userBase(ctx)}/messages`, message(p, together ? all : []));
|
|
274
|
+
return finishDraft(ctx, created, together ? [] : all, false);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
rl.registerAction("draft.send", {
|
|
278
|
+
access: "write",
|
|
279
|
+
description: "Send a draft message. Returns {success}.",
|
|
280
|
+
inputSchema: byId,
|
|
281
|
+
async execute(input, ctx) {
|
|
282
|
+
const { id } = input;
|
|
283
|
+
await graph(ctx, "POST", `${messagePath(ctx, id)}/send`);
|
|
87
284
|
return { success: true };
|
|
88
285
|
},
|
|
89
286
|
});
|
|
90
|
-
rl.registerAction("mail.
|
|
287
|
+
rl.registerAction("mail.reply", {
|
|
91
288
|
access: "write",
|
|
92
|
-
description: "
|
|
93
|
-
inputSchema:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
289
|
+
description: "Reply to a message, or with replyAll to everyone on it, with attachments. Returns {success}, or {id, webLink} of the draft when send is false.",
|
|
290
|
+
inputSchema: reply,
|
|
291
|
+
async execute(input, ctx) {
|
|
292
|
+
const p = input;
|
|
293
|
+
const all = await files(p.attachments);
|
|
294
|
+
const created = await graph(ctx, "POST", `${messagePath(ctx, p.id)}/${p.replyAll ? "createReplyAll" : "createReply"}`, p.comment === undefined ? {} : { comment: p.comment });
|
|
295
|
+
return finishDraft(ctx, created, all, p.send ?? true);
|
|
99
296
|
},
|
|
297
|
+
});
|
|
298
|
+
rl.registerAction("mail.forward", {
|
|
299
|
+
access: "write",
|
|
300
|
+
description: "Forward a message to more people, with attachments. Returns {success}, or {id, webLink} of the draft when send is false.",
|
|
301
|
+
inputSchema: forward,
|
|
100
302
|
async execute(input, ctx) {
|
|
101
|
-
const
|
|
102
|
-
|
|
303
|
+
const p = input;
|
|
304
|
+
const all = await files(p.attachments);
|
|
305
|
+
const created = await graph(ctx, "POST", `${messagePath(ctx, p.id)}/createForward`, {
|
|
306
|
+
toRecipients: recipients(p.to),
|
|
307
|
+
...(p.comment === undefined ? {} : { comment: p.comment }),
|
|
308
|
+
});
|
|
309
|
+
return finishDraft(ctx, created, all, p.send ?? true);
|
|
103
310
|
},
|
|
104
311
|
});
|
|
105
312
|
rl.registerAction("mail.list", {
|
|
106
313
|
access: "read",
|
|
107
|
-
description: "List recent messages
|
|
108
|
-
inputSchema:
|
|
109
|
-
search: {
|
|
110
|
-
type: "string",
|
|
111
|
-
required: false,
|
|
112
|
-
description: "KQL search across the mailbox",
|
|
113
|
-
},
|
|
114
|
-
top: { type: "number", required: false, default: 20 },
|
|
115
|
-
},
|
|
314
|
+
description: "List recent messages, or search with KQL (results come newest first). Returns [{id,subject,from,receivedDateTime,bodyPreview,hasAttachments}].",
|
|
315
|
+
inputSchema: list,
|
|
116
316
|
async execute(input, ctx) {
|
|
117
317
|
const p = input;
|
|
118
318
|
const qs = new URLSearchParams({
|
|
119
319
|
$top: String(p.top ?? 20),
|
|
120
320
|
$select: "id,subject,from,receivedDateTime,bodyPreview,hasAttachments",
|
|
121
|
-
$orderby: "receivedDateTime desc",
|
|
122
321
|
});
|
|
123
322
|
if (p.search)
|
|
124
323
|
qs.set("$search", `"${p.search}"`);
|
|
125
|
-
|
|
324
|
+
else
|
|
325
|
+
qs.set("$orderby", "receivedDateTime desc");
|
|
326
|
+
const r = await graph(ctx, "GET", `${userBase(ctx)}/messages?${qs}`);
|
|
126
327
|
return r.value;
|
|
127
328
|
},
|
|
128
329
|
});
|
|
129
330
|
rl.registerAction("mail.get", {
|
|
130
331
|
access: "read",
|
|
131
|
-
description: "Get one message with full body by id.",
|
|
132
|
-
inputSchema:
|
|
332
|
+
description: "Get one message with full body by id. Its files are listed by attachment.list.",
|
|
333
|
+
inputSchema: byId,
|
|
334
|
+
async execute(input, ctx) {
|
|
335
|
+
const { id } = input;
|
|
336
|
+
return graph(ctx, "GET", messagePath(ctx, id));
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
rl.registerAction("attachment.add", {
|
|
340
|
+
access: "write",
|
|
341
|
+
description: "Attach a file to a draft message. Returns {id, name, size}.",
|
|
342
|
+
inputSchema: attachmentAdd,
|
|
343
|
+
async execute(input, ctx) {
|
|
344
|
+
const p = input;
|
|
345
|
+
const [file] = await files([p.attachment]);
|
|
346
|
+
return attach(ctx, p.messageId, file);
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
rl.registerAction("attachment.list", {
|
|
350
|
+
access: "read",
|
|
351
|
+
description: "List a message's attachments. Returns [{id,name,contentType,size,isInline}].",
|
|
352
|
+
inputSchema: attachmentList,
|
|
353
|
+
async execute(input, ctx) {
|
|
354
|
+
const { messageId } = input;
|
|
355
|
+
const r = await graph(ctx, "GET", `${messagePath(ctx, messageId)}/attachments?$select=id,name,contentType,size,isInline`);
|
|
356
|
+
return r.value;
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
rl.registerAction("attachment.get", {
|
|
360
|
+
access: "read",
|
|
361
|
+
description: "Download one file attachment. Returns {name, mimeType, size, contentBase64}, accepted as-is as an attachment by mail plugins. A link (reference) attachment has no bytes and fails with HTTP 405.",
|
|
362
|
+
inputSchema: attachmentRef,
|
|
133
363
|
async execute(input, ctx) {
|
|
134
364
|
const p = input;
|
|
135
|
-
|
|
365
|
+
const path = `${messagePath(ctx, p.messageId)}/attachments/${pathSegment(p.attachmentId)}`;
|
|
366
|
+
const meta = await graph(ctx, "GET", `${path}?$select=name,contentType`);
|
|
367
|
+
const res = await graphResponse(ctx, NAME, SCOPES, "GET", `${path}/$value`, undefined, undefined, "*/*");
|
|
368
|
+
if (!res.ok)
|
|
369
|
+
throw await graphFailed(NAME, res);
|
|
370
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
371
|
+
return {
|
|
372
|
+
...(meta.name ? { name: meta.name } : {}),
|
|
373
|
+
...(meta.contentType ? { mimeType: meta.contentType } : {}),
|
|
374
|
+
size: bytes.length,
|
|
375
|
+
contentBase64: bytes.toString("base64"),
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
rl.registerAction("attachment.delete", {
|
|
380
|
+
access: "write",
|
|
381
|
+
description: "Remove an attachment from a draft message. Returns {success}.",
|
|
382
|
+
inputSchema: attachmentRef,
|
|
383
|
+
async execute(input, ctx) {
|
|
384
|
+
const p = input;
|
|
385
|
+
await graph(ctx, "DELETE", `${messagePath(ctx, p.messageId)}/attachments/${pathSegment(p.attachmentId)}`);
|
|
386
|
+
return { success: true };
|
|
136
387
|
},
|
|
137
388
|
});
|
|
138
389
|
}
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { staticCredential } from "runline";
|
|
2
|
-
/**
|
|
3
|
-
* An API token, sent as a bearer to Todoist's REST v2 and Sync v9
|
|
4
|
-
* surfaces on the one API origin.
|
|
5
|
-
*/
|
|
2
|
+
/** An API token, sent as a bearer to Todoist's unified API v1. */
|
|
6
3
|
export const todoistCredential = staticCredential({
|
|
7
4
|
id: "todoist",
|
|
8
5
|
auth: { kind: "bearer" },
|
|
9
6
|
local: { secret: "apiToken" },
|
|
10
7
|
targets: {
|
|
11
|
-
|
|
12
|
-
baseUrl: "https://api.todoist.com/
|
|
8
|
+
api: {
|
|
9
|
+
baseUrl: "https://api.todoist.com/api/v1/",
|
|
13
10
|
methods: ["GET", "POST", "DELETE"],
|
|
14
11
|
},
|
|
15
|
-
sync: {
|
|
16
|
-
baseUrl: "https://api.todoist.com/sync/v9/",
|
|
17
|
-
methods: ["POST"],
|
|
18
|
-
},
|
|
19
12
|
},
|
|
20
13
|
probe: {
|
|
21
|
-
target: "
|
|
14
|
+
target: "api",
|
|
22
15
|
path: "projects",
|
|
23
16
|
method: "GET",
|
|
24
17
|
acceptedStatuses: [200],
|