@zapier/microsoft-outlook-connector 0.0.0 → 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +93 -0
  2. package/NOTICE +8 -0
  3. package/NOTICE.md +8 -0
  4. package/README.md +145 -2
  5. package/SKILL.md +159 -0
  6. package/cli.js +71 -0
  7. package/cli.ts +5 -0
  8. package/connections.ts +69 -0
  9. package/dist/cli.js +4 -0
  10. package/dist/index.js +1823 -0
  11. package/index.ts +103 -0
  12. package/package.json +65 -4
  13. package/preflight.sh +157 -0
  14. package/references/microsoft-outlook-api-gotchas.md +210 -0
  15. package/scripts/copyMessage.ts +68 -0
  16. package/scripts/createContact.ts +39 -0
  17. package/scripts/createDraft.ts +71 -0
  18. package/scripts/createEvent.ts +61 -0
  19. package/scripts/createMailFolder.ts +68 -0
  20. package/scripts/createReplyDraft.ts +81 -0
  21. package/scripts/deleteContact.ts +47 -0
  22. package/scripts/deleteEvent.ts +61 -0
  23. package/scripts/deleteMessage.ts +57 -0
  24. package/scripts/forwardMessage.ts +75 -0
  25. package/scripts/getAttachment.ts +60 -0
  26. package/scripts/getContact.ts +44 -0
  27. package/scripts/getEvent.ts +63 -0
  28. package/scripts/getMe.ts +42 -0
  29. package/scripts/getMessage.ts +68 -0
  30. package/scripts/listAttachments.ts +94 -0
  31. package/scripts/listCalendarView.ts +99 -0
  32. package/scripts/listCalendars.ts +85 -0
  33. package/scripts/listCategories.ts +49 -0
  34. package/scripts/listContacts.ts +81 -0
  35. package/scripts/listEvents.ts +94 -0
  36. package/scripts/listMailFolders.ts +98 -0
  37. package/scripts/listMessages.ts +106 -0
  38. package/scripts/moveMessage.ts +68 -0
  39. package/scripts/replyToMessage.ts +73 -0
  40. package/scripts/sendDraft.ts +55 -0
  41. package/scripts/sendMail.ts +68 -0
  42. package/scripts/updateContact.ts +49 -0
  43. package/scripts/updateEvent.ts +69 -0
  44. package/scripts/updateMessage.ts +99 -0
  45. package/tsup.config.ts +63 -0
package/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { defineConnector, toFunctions } from "@zapier/connectors-sdk";
2
+
3
+ import { connectionResolvers } from "./connections.ts";
4
+ import copyMessageDefinition from "./scripts/copyMessage.ts";
5
+ import createContactDefinition from "./scripts/createContact.ts";
6
+ import createDraftDefinition from "./scripts/createDraft.ts";
7
+ import createEventDefinition from "./scripts/createEvent.ts";
8
+ import createMailFolderDefinition from "./scripts/createMailFolder.ts";
9
+ import createReplyDraftDefinition from "./scripts/createReplyDraft.ts";
10
+ import deleteContactDefinition from "./scripts/deleteContact.ts";
11
+ import deleteEventDefinition from "./scripts/deleteEvent.ts";
12
+ import deleteMessageDefinition from "./scripts/deleteMessage.ts";
13
+ import forwardMessageDefinition from "./scripts/forwardMessage.ts";
14
+ import getAttachmentDefinition from "./scripts/getAttachment.ts";
15
+ import getContactDefinition from "./scripts/getContact.ts";
16
+ import getEventDefinition from "./scripts/getEvent.ts";
17
+ import getMeDefinition from "./scripts/getMe.ts";
18
+ import getMessageDefinition from "./scripts/getMessage.ts";
19
+ import listAttachmentsDefinition from "./scripts/listAttachments.ts";
20
+ import listCalendarsDefinition from "./scripts/listCalendars.ts";
21
+ import listCalendarViewDefinition from "./scripts/listCalendarView.ts";
22
+ import listCategoriesDefinition from "./scripts/listCategories.ts";
23
+ import listContactsDefinition from "./scripts/listContacts.ts";
24
+ import listEventsDefinition from "./scripts/listEvents.ts";
25
+ import listMailFoldersDefinition from "./scripts/listMailFolders.ts";
26
+ import listMessagesDefinition from "./scripts/listMessages.ts";
27
+ import moveMessageDefinition from "./scripts/moveMessage.ts";
28
+ import replyToMessageDefinition from "./scripts/replyToMessage.ts";
29
+ import sendDraftDefinition from "./scripts/sendDraft.ts";
30
+ import sendMailDefinition from "./scripts/sendMail.ts";
31
+ import updateContactDefinition from "./scripts/updateContact.ts";
32
+ import updateEventDefinition from "./scripts/updateEvent.ts";
33
+ import updateMessageDefinition from "./scripts/updateMessage.ts";
34
+
35
+ const connector = defineConnector({
36
+ scripts: {
37
+ copyMessage: copyMessageDefinition,
38
+ createContact: createContactDefinition,
39
+ createDraft: createDraftDefinition,
40
+ createEvent: createEventDefinition,
41
+ createMailFolder: createMailFolderDefinition,
42
+ createReplyDraft: createReplyDraftDefinition,
43
+ deleteContact: deleteContactDefinition,
44
+ deleteEvent: deleteEventDefinition,
45
+ deleteMessage: deleteMessageDefinition,
46
+ forwardMessage: forwardMessageDefinition,
47
+ getAttachment: getAttachmentDefinition,
48
+ getContact: getContactDefinition,
49
+ getEvent: getEventDefinition,
50
+ getMe: getMeDefinition,
51
+ getMessage: getMessageDefinition,
52
+ listAttachments: listAttachmentsDefinition,
53
+ listCalendars: listCalendarsDefinition,
54
+ listCalendarView: listCalendarViewDefinition,
55
+ listCategories: listCategoriesDefinition,
56
+ listContacts: listContactsDefinition,
57
+ listEvents: listEventsDefinition,
58
+ listMailFolders: listMailFoldersDefinition,
59
+ listMessages: listMessagesDefinition,
60
+ moveMessage: moveMessageDefinition,
61
+ replyToMessage: replyToMessageDefinition,
62
+ sendDraft: sendDraftDefinition,
63
+ sendMail: sendMailDefinition,
64
+ updateContact: updateContactDefinition,
65
+ updateEvent: updateEventDefinition,
66
+ updateMessage: updateMessageDefinition,
67
+ },
68
+ connectionResolvers,
69
+ });
70
+
71
+ export default connector;
72
+ export const {
73
+ copyMessage,
74
+ createContact,
75
+ createDraft,
76
+ createEvent,
77
+ createMailFolder,
78
+ createReplyDraft,
79
+ deleteContact,
80
+ deleteEvent,
81
+ deleteMessage,
82
+ forwardMessage,
83
+ getAttachment,
84
+ getContact,
85
+ getEvent,
86
+ getMe,
87
+ getMessage,
88
+ listAttachments,
89
+ listCalendars,
90
+ listCalendarView,
91
+ listCategories,
92
+ listContacts,
93
+ listEvents,
94
+ listMailFolders,
95
+ listMessages,
96
+ moveMessage,
97
+ replyToMessage,
98
+ sendDraft,
99
+ sendMail,
100
+ updateContact,
101
+ updateEvent,
102
+ updateMessage,
103
+ } = toFunctions(connector);
package/package.json CHANGED
@@ -1,7 +1,68 @@
1
1
  {
2
2
  "name": "@zapier/microsoft-outlook-connector",
3
- "version": "0.0.0",
4
- "description": "Placeholder published to enable OIDC Trusted Publisher setup. The real package is published via GitLab CI.",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Agent-callable Microsoft Outlook tools — read and search mail, send/reply/forward, organize messages and folders, manage calendar events, and manage contacts. Use when the user mentions Outlook, Microsoft 365 mail/calendar/contacts, or wants to send, read, search, or organize email, schedule events, or look up contacts — even if they don't name Outlook explicitly.",
5
8
  "license": "Elastic-2.0",
6
- "private": false
7
- }
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./index.ts"
14
+ }
15
+ },
16
+ "bin": {
17
+ "@zapier/microsoft-outlook-connector": "./cli.js"
18
+ },
19
+ "files": [
20
+ "dist/",
21
+ "cli.js",
22
+ "*.ts",
23
+ "scripts/",
24
+ "preflight.sh",
25
+ "SKILL.md",
26
+ "README.md",
27
+ "LICENSE",
28
+ "NOTICE.md",
29
+ "references/",
30
+ "NOTICE"
31
+ ],
32
+ "dependencies": {
33
+ "@zapier/connectors-sdk": "^0.1.0",
34
+ "zod": "^4.0.0",
35
+ "@modelcontextprotocol/sdk": "^1.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@zapier/zapier-sdk": ">=0.59.0 <1.0.0"
39
+ },
40
+ "keywords": [
41
+ "microsoft-outlook",
42
+ "outlook",
43
+ "email",
44
+ "calendar",
45
+ "contacts",
46
+ "microsoft-365",
47
+ "zapier",
48
+ "connector",
49
+ "tools",
50
+ "skills",
51
+ "mcp",
52
+ "agent",
53
+ "ai",
54
+ "automation"
55
+ ],
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/zapier/connectors.git",
59
+ "directory": "apps/microsoft-outlook"
60
+ },
61
+ "devDependencies": {
62
+ "tsup": "^8.0.0",
63
+ "typescript": "^5.0.0"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup"
67
+ }
68
+ }
package/preflight.sh ADDED
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env sh
2
+ # Connector pre-flight readiness check.
3
+ #
4
+ # Managed by @zapier/connectors-dev — do not edit; synced byte-for-byte
5
+ # across every connector.
6
+ #
7
+ # Runs inside whatever agent harness installed the connector (Cursor, Claude
8
+ # Code, Codex, Gemini CLI, Goose, ...) — often a minimal container — and answers
9
+ # ONE question: how do I run the TypeScript scripts here? It picks a runtime —
10
+ # Node 22.18+ when it can already resolve the connector's deps, else an explicit
11
+ # install step (`npm install`, or `bun install` when only Bun is present) — and
12
+ # tells the agent the exact command to run (see EXIT CODES). When deps are
13
+ # missing it disambiguates the two sandbox failures that block an install: a
14
+ # read-only connector dir (must run unsandboxed / be granted write) vs. a
15
+ # blocked home dir (point the package cache inside this dir). Both surface as a
16
+ # misleading `EPERM`, so the recommendation names the actual fix.
17
+ #
18
+ # NEEDS_ACTION is a single self-verifying step (install a runtime / deps), not a
19
+ # loop: do it, then run a script. The action confirms its own success and the
20
+ # first `--help` run is the authoritative check, so re-running this pre-flight to
21
+ # reconfirm is optional, not required.
22
+ #
23
+ # THE AGENT CONTRACT IS THE STDOUT, NOT THIS HEADER. Agents don't read this file;
24
+ # they run it and parse the `PREFLIGHT_*` lines — each value starts with a stable
25
+ # token (parse as `KEY: (\w+)`), with an optional human gloss in parens, and
26
+ # `PREFLIGHT_RECOMMENDATION` is the one-line next step. SKILL.md "Step 0" is the
27
+ # agent-facing spec; this header is for maintainers of the canonical script.
28
+ #
29
+ # WHY POSIX sh (not bash)
30
+ # Minimal sandboxes often ship only BusyBox `sh` with no bash. This script runs
31
+ # unchanged under BusyBox sh, dash, and bash, and never hard-requires
32
+ # node/bun/npm — a missing runtime degrades to a NEEDS_ACTION instruction.
33
+ #
34
+ # EXIT CODES (the verdict; also emitted on PREFLIGHT_STATUS)
35
+ # 0 READY a runtime + deps are in place; run the scripts
36
+ # 1 NEEDS_ACTION perform the printed action (install runtime / deps), then
37
+ # run a script — re-running this check is optional
38
+
39
+ set -u
40
+
41
+ EXIT_READY=0
42
+ EXIT_NEEDS_ACTION=1
43
+
44
+ # Directory this script lives in — deps + scripts are resolved relative to it,
45
+ # not the caller's cwd, so `./preflight.sh` works from anywhere.
46
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
47
+
48
+ has() {
49
+ command -v "$1" >/dev/null 2>&1
50
+ }
51
+
52
+ # Node >= 22.18 is the connector baseline (native .ts stripping). Anything older
53
+ # is treated as "no Node" so we fall back to Bun.
54
+ node_ge_2218() {
55
+ has node || return 1
56
+ v=$(node -v 2>/dev/null) || return 1
57
+ v=${v#v}
58
+ major=${v%%.*}
59
+ rest=${v#*.}
60
+ minor=${rest%%.*}
61
+ case "$major" in '' | *[!0-9]*) return 1 ;; esac
62
+ case "$minor" in '' | *[!0-9]*) minor=0 ;; esac
63
+ [ "$major" -gt 22 ] && return 0
64
+ [ "$major" -eq 22 ] && [ "$minor" -ge 18 ] && return 0
65
+ return 1
66
+ }
67
+
68
+ # Are the connector's declared deps installed where Node would find them? Node
69
+ # won't fetch — they must be on disk (this dir's node_modules or an ancestor's).
70
+ # Reads the connector's own package.json, so this stays connector-agnostic. A
71
+ # bare `[ -d node_modules ]` is the wrong test: under pnpm/monorepo layouts the
72
+ # deps can live in an ancestor (or be hoisted), and a local node_modules can
73
+ # exist without the package being present. We check each dep's package.json
74
+ # exists in one of Node's resolution paths rather than `require.resolve(name)`,
75
+ # because resolving the package ENTRY can fail for ESM-only / `exports`-map
76
+ # packages even when they're fully installed and importable.
77
+ node_resolves() {
78
+ ( CDPATH= cd -- "$SCRIPT_DIR" && node -e 'const fs=require("fs"),path=require("path");const d=require("./package.json").dependencies||{};for(const k of Object.keys(d)){const ps=require.resolve.paths(k)||[];if(!ps.some(b=>fs.existsSync(path.join(b,k,"package.json"))))process.exit(1);}' ) >/dev/null 2>&1
79
+ }
80
+
81
+ # Can we actually WRITE into this directory right now? Any dep install must land
82
+ # `node_modules/` here, so if the harness mounts the connector read-only (common
83
+ # when skills live under ~/.<agent>/skills, outside the agent's writable
84
+ # workspace) no install can succeed in place — the only fixes are to run it
85
+ # unsandboxed or grant write access. Two deliberate choices:
86
+ # - Probe with a real create+remove, not `[ -w ]`: a sandbox denies the write
87
+ # at the syscall while the permission bits still look writable.
88
+ # - Probe by creating a DIRECTORY (`mkdir`), not a file: that's the install's
89
+ # very first on-disk action (node_modules/ and the cache dirs), and at least
90
+ # one sandbox (Claude Code) permits creating a file here while denying
91
+ # `mkdir` — a file-based probe reports writable and the install then EPERMs.
92
+ dir_writable() {
93
+ _t="$SCRIPT_DIR/.preflight-write-test.$$"
94
+ mkdir "$_t" 2>/dev/null || return 1
95
+ rmdir "$_t" 2>/dev/null
96
+ return 0
97
+ }
98
+
99
+ # ---- 1) Pick a runtime -----------------------------------------------------
100
+ # Node 22.18+ (native TS strip) is the baseline and the preferred runner whenever
101
+ # it's present — it runs the .ts scripts directly. Bun is the fallback runner
102
+ # only when there's no usable Node. We DON'T lean on Bun's implicit auto-install:
103
+ # it's silently suppressed by any ancestor node_modules (monorepo layouts) and
104
+ # fails the same way Node's `npm install` does under a sandbox that blocks Bun's
105
+ # home cache — so missing deps are always an explicit, cache-local install step.
106
+ nresolves=false
107
+ node_ge_2218 && node_resolves && nresolves=true
108
+
109
+ if [ "$nresolves" = true ]; then
110
+ runner=node
111
+ ready=true
112
+ elif node_ge_2218; then
113
+ runner=node # present but deps unresolved — install step below
114
+ ready=false
115
+ elif has bun; then
116
+ runner=bun # no usable Node — Bun runs the scripts; install step below
117
+ ready=false
118
+ else
119
+ echo "PREFLIGHT_STATUS: NEEDS_ACTION"
120
+ echo "PREFLIGHT_RECOMMENDATION: no Node 22.18+ or Bun found — install Node 22.18+ (ships npm) or Bun, then run \`<runner> ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
121
+ exit "$EXIT_NEEDS_ACTION"
122
+ fi
123
+
124
+ # ---- 2) Install deps if the chosen runtime can't run as-is -----------------
125
+ # Two distinct sandbox failures to disambiguate, because their fixes differ:
126
+ # a) This directory is READ-ONLY (skills installed outside the agent's
127
+ # writable workspace). No install can place node_modules here — the agent
128
+ # must run it unsandboxed or be granted write access. A cache flag can't
129
+ # help.
130
+ # b) The directory is writable but the HOME dir is sandboxed, so the default
131
+ # ~/.npm / ~/.bun cache write is what EPERMs mid-install. Pointing the cache
132
+ # INSIDE this directory sidesteps that without disabling the sandbox (and is
133
+ # harmless otherwise — just an unused cache dir).
134
+ # So probe writability first and recommend accordingly.
135
+ if [ "$ready" != true ]; then
136
+ echo "PREFLIGHT_STATUS: NEEDS_ACTION"
137
+ [ "$runner" = bun ] && install_cmd="bun install" || install_cmd="npm install"
138
+ if ! dir_writable; then
139
+ echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed and ${SCRIPT_DIR} is read-only in the current sandbox (a test write there failed) — \`${install_cmd}\` can't place node_modules here. Run the install with the sandbox disabled, or grant the agent write access to ${SCRIPT_DIR} (a cache flag won't help — node_modules must land in this directory); then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
140
+ elif [ "$runner" = bun ]; then
141
+ echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed — run \`BUN_INSTALL_CACHE_DIR=\"${SCRIPT_DIR}/.bun-cache\" bun install\` in ${SCRIPT_DIR} (the workspace-local cache survives a sandbox that blocks ~/.bun; plain \`bun install\` works otherwise), then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
142
+ elif has npm; then
143
+ echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed — run \`npm install --cache \"${SCRIPT_DIR}/.npm-cache\"\` in ${SCRIPT_DIR} (the workspace-local --cache survives a sandbox that blocks ~/.npm; plain \`npm install\` works otherwise), then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
144
+ else
145
+ # node >= 22.18 ships npm, so a missing npm means it was removed from the
146
+ # Node install. Restore it, then install with a workspace-local cache.
147
+ echo "PREFLIGHT_RECOMMENDATION: npm is missing (it ships with Node 22.18+) — reinstall/repair Node 22.18+, run \`npm install --cache \"${SCRIPT_DIR}/.npm-cache\"\` in ${SCRIPT_DIR}, then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
148
+ fi
149
+ exit "$EXIT_NEEDS_ACTION"
150
+ fi
151
+
152
+ # ---- 3) Ready --------------------------------------------------------------
153
+ # Runtime + deps are in place — the scripts run.
154
+ echo "PREFLIGHT_STATUS: READY"
155
+ echo "PREFLIGHT_RUNNER: ${runner}"
156
+ echo "PREFLIGHT_RECOMMENDATION: run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials, then run the script with the required env vars set."
157
+ exit "$EXIT_READY"
@@ -0,0 +1,210 @@
1
+ # Microsoft Outlook (Microsoft Graph) — API gotchas
2
+
3
+ Behavioral notes for the Outlook tools, which call the **Microsoft Graph v1.0** REST API
4
+ (`https://graph.microsoft.com/v1.0`). Every claim here is sourced from the public Microsoft
5
+ Graph documentation; links are inline. Load this when a call returns an error you don't
6
+ understand, when ids stop resolving, or when paging / search / date-time handling behaves
7
+ unexpectedly.
8
+
9
+ ## Authentication & identity
10
+
11
+ - Every request is authorized with a single OAuth 2.0 **bearer token** (`Authorization: Bearer
12
+ <token>`); there is no bot/user split. Use **getMe** as the auth probe — a `200` confirms the
13
+ connection works and returns your `id`, `displayName`, `mail`, and `userPrincipalName`.
14
+ - Resolve **"my email"** with getMe before composing or filtering, since the signed-in user's
15
+ address isn't otherwise known to the tools.
16
+
17
+ ## Item ids change when items move — immutable ids are requested for you
18
+
19
+ By default a message/event/contact **id changes when the item is moved** from one container to
20
+ another: _"By default, this value changes when the item is moved from one container (such as a
21
+ folder or calendar) to another."_ ([message resource](https://learn.microsoft.com/en-us/graph/api/resources/message),
22
+ [event resource](https://learn.microsoft.com/en-us/graph/api/resources/event)) This is the dominant
23
+ cause of stale-id `404`s.
24
+
25
+ This connector sends `Prefer: IdType="ImmutableId"` on **every** request, so the ids you get back
26
+ are stable: _"Immutable identifiers (IDs) enable your application to obtain an ID that doesn't
27
+ change for the lifetime of the item"_ and _"will NOT change if the item is moved to a different
28
+ folder in the mailbox."_ ([immutable ids](https://learn.microsoft.com/en-us/graph/outlook-immutable-id))
29
+ The immutable id still changes if the item is moved to an **archive mailbox** or exported and
30
+ re-imported (same source).
31
+
32
+ - **moveMessage** returns the message in its new folder; use the id from that response for any
33
+ follow-up call. ([move message](https://learn.microsoft.com/en-us/graph/api/resources/message) — _"Move
34
+ the message to a folder. This creates a new copy of the message in the destination folder."_)
35
+ - Ids are **case-sensitive**: _"all identifiers in Microsoft Graph, are case-sensitive."_
36
+ ([immutable ids](https://learn.microsoft.com/en-us/graph/outlook-immutable-id))
37
+ - A sent message can be located in **Sent Items** by creating the draft with the immutable-id
38
+ header and reusing that id after sending. ([immutable ids](https://learn.microsoft.com/en-us/graph/outlook-immutable-id))
39
+
40
+ ## Shared mailboxes
41
+
42
+ Pass a `mailbox` (UPN/email) to act on another user's mailbox via `/users/{id|userPrincipalName}`.
43
+ This only works when that user has **shared the folder or delegated access** to the signed-in
44
+ user: _"If Garth hasn't shared his Inbox with John, nor has he delegated his mailbox to John,
45
+ specifying Garth's user ID or user principal name in those GET operations return an error."_
46
+ ([shared/delegated folders](https://learn.microsoft.com/en-us/graph/outlook-share-messages-folders)).
47
+ Reading/writing a shared folder uses the `Mail.Read.Shared` / `Mail.ReadWrite.Shared` delegated
48
+ permissions (same source). Sending **from** a shared mailbox relies on Exchange-side delegation —
49
+ the message `sender`/`from` _"can be set to a different value when sending a message from a shared
50
+ mailbox … or as a delegate."_ ([message resource](https://learn.microsoft.com/en-us/graph/api/resources/message))
51
+
52
+ ## Paging — `next_cursor` is an opaque URL
53
+
54
+ List tools return `next_cursor`, which is Graph's `@odata.nextLink`. Pass it back verbatim as
55
+ `cursor`; don't parse or rebuild it: _"Use the entire URL in the `@odata.nextLink` property in a
56
+ GET request to retrieve the next page of results … Don't try to extract the `$skiptoken` or
57
+ `$skip` value and use it in a different request."_ ([paging](https://learn.microsoft.com/en-us/graph/paging))
58
+
59
+ - **listMessages** default page size is **10**, max **1000**: _"The default page size is 10
60
+ messages. Use `$top` to customize the page size, within the range of 1 and 1000."_
61
+ ([list messages](https://learn.microsoft.com/en-us/graph/api/user-list-messages))
62
+ - **listCategories** is **not paged** — `masterCategories` returns the full set in one response.
63
+ ([list masterCategories](https://learn.microsoft.com/en-us/graph/api/outlookuser-list-mastercategories))
64
+
65
+ ## Search vs. filter (listMessages, listContacts, listEvents)
66
+
67
+ - **`search`** is a full-text KQL query over messages. With no property prefix it searches the
68
+ default properties **from, subject, body**; you can also target a property (`subject:invoice`,
69
+ `from:acme`). Message search results are _"sorted by the date and time the message was sent"_ and
70
+ _"A `$search` request returns up to 1,000 results."_
71
+ ([$search](https://learn.microsoft.com/en-us/graph/search-query-parameter)) Because the order is
72
+ fixed to sent date/time, don't expect to control ordering while searching.
73
+ - **`filter`** is an OData `$filter` for exact matches (`isRead eq false`, `importance eq 'high'`).
74
+ When `$filter` and `$orderby` are combined on messages, _"Properties that appear in `$orderby`
75
+ must also appear in `$filter`"_ and in the same order.
76
+ ([list messages](https://learn.microsoft.com/en-us/graph/api/user-list-messages))
77
+ - **listContacts** filtering is **limited to email-address match**: _"You can use `$filter`, `any`,
78
+ and the `eq` operator on only the **address** sub-property of instances in an **emailAddresses**
79
+ collection. That is, you can't filter on the **name** or any other sub-property … nor can you
80
+ apply any other operator or function with `filter`, such as `ne`, `le`, and `startswith()`."_
81
+ ([list contacts](https://learn.microsoft.com/en-us/graph/api/user-list-contacts)) For name lookups,
82
+ list and match the returned results client-side.
83
+
84
+ ## Messages
85
+
86
+ - **sendMail returns `202 Accepted` with no body and no message id.** _"If successful, this method
87
+ returns `202 Accepted` response code. It doesn't return anything in the response body"_ and
88
+ _"A `202 Accepted` response code … doesn't indicate that the request processing has completed.
89
+ Delivery of the message is subject to Exchange Online limitations and throttling."_
90
+ ([sendMail](https://learn.microsoft.com/en-us/graph/api/user-sendmail)) When you need the sent
91
+ message's id, use **createDraft** (returns the draft id) then **sendDraft**.
92
+ - **saveToSentItems** defaults to true: _"Specify it only if the parameter is false; default is
93
+ true."_ (same source) Reply, reply-all, and forward also save to Sent Items.
94
+ ([message resource](https://learn.microsoft.com/en-us/graph/api/resources/message))
95
+ - **createReplyDraft / forward draft** create a draft pre-populated with the original recipients
96
+ and quoted body that you can edit and send later: _"Create a draft to reply to the sender of a
97
+ message … You can update the draft later … Send the draft message in a subsequent operation."_
98
+ ([createReply](https://learn.microsoft.com/en-us/graph/api/message-createreply))
99
+ - **deleteMessage is a soft delete.** The message moves to the **Deleted Items** folder —
100
+ _"deleteditems: The folder items are moved to when they're deleted"_ — and `deleteMessage`
101
+ returns `204 No Content`. A separate _"Permanently delete"_ operation purges items; this
102
+ connector does not expose it. ([mailFolder](https://learn.microsoft.com/en-us/graph/api/resources/mailfolder),
103
+ [delete message](https://learn.microsoft.com/en-us/graph/api/message-delete))
104
+ - **updateMessage** is a PATCH: only the fields you send change. **categories REPLACE** the existing
105
+ set (read current via getMessage and include them to append). Subject/body are editable only on
106
+ drafts.
107
+ - **bodyPreview** is _"The first 255 characters of the message body. It is in text format."_ — use
108
+ getMessage for the full body. ([message resource](https://learn.microsoft.com/en-us/graph/api/resources/message))
109
+ - **getMessage** body format is controlled by `Prefer: outlook.body-content-type` (text or html);
110
+ this connector defaults to text. ([list messages](https://learn.microsoft.com/en-us/graph/api/user-list-messages))
111
+
112
+ ### Well-known folder names
113
+
114
+ `folderId` / `destinationId` / `parentFolderId` accept either a folder id or a well-known name —
115
+ _"Instead of using the corresponding folder id value, for convenience, you can use the well-known
116
+ folder names."_ Common ones: `inbox`, `drafts`, `sentitems`, `deleteditems`, `archive`, `junkemail`.
117
+ ([mailFolder](https://learn.microsoft.com/en-us/graph/api/resources/mailfolder))
118
+
119
+ ## Attachments
120
+
121
+ - `hasAttachments` is **false when a message has only inline attachments**: _"This property doesn't
122
+ include inline attachments, so if a message contains only inline attachments, this property is
123
+ false."_ Still call **listAttachments** to be sure. ([message resource](https://learn.microsoft.com/en-us/graph/api/resources/message))
124
+ - An attachment is one of three kinds — a **file** (`fileAttachment`), an **item** (`itemAttachment`),
125
+ or a **reference** (`referenceAttachment`). ([attachment](https://learn.microsoft.com/en-us/graph/api/resources/attachment))
126
+ - **getAttachment** returns `contentBytes` (_"The base64-encoded contents of the file"_) only for
127
+ **file** attachments — item and reference attachments carry no inline bytes.
128
+ ([fileAttachment](https://learn.microsoft.com/en-us/graph/api/resources/fileattachment))
129
+ - **Outgoing attachments must be under 3 MB** and are sent inline (base64) in the same request:
130
+ _"If the file size is under 3 MB, do a single POST on the attachments navigation property … If
131
+ the file size is between 3 MB and 150 MB, create an upload session."_
132
+ ([large attachments](https://learn.microsoft.com/en-us/graph/outlook-large-attachments)) This
133
+ connector supports only the under-3-MB inline path; larger files (which require an upload session)
134
+ are not supported and surface as a `413`.
135
+
136
+ ## Calendar
137
+
138
+ - **listEvents** returns single events **plus recurring series masters** (the recurrence
139
+ definition), not expanded occurrences: _"The list contains single instance meetings and series
140
+ masters."_ ([event resource](https://learn.microsoft.com/en-us/graph/api/resources/event))
141
+ - **listCalendarView** expands recurring series into individual occurrences over a time window:
142
+ _"Get the occurrences, exceptions and single instances of events in a calendar view defined by a
143
+ time range."_ Its `startDateTime`/`endDateTime` are interpreted by their offset, **defaulting to
144
+ UTC** when none is given: _"If no timezone offset is included in the value, it is interpreted as
145
+ UTC."_ (`$top` range is 1–1000.) ([list calendarView](https://learn.microsoft.com/en-us/graph/api/calendar-list-calendarview))
146
+ - **Event start/end** are `{ dateTime, timeZone }`. `dateTime` is a **naive local timestamp** in
147
+ the form `{date}T{time}` (e.g. `2026-07-01T15:30:00`) with **no trailing Z or offset** — the zone
148
+ goes in the separate `timeZone` field. `timeZone` accepts a **Windows name** (e.g. _"Pacific
149
+ Standard Time"_) _"as well as the other time zones supported by the calendar API"_ (IANA names
150
+ such as `America/Los_Angeles` are listed). ([dateTimeTimeZone](https://learn.microsoft.com/en-us/graph/api/resources/datetimetimezone))
151
+ - **All-day events**: _"If true, regardless of whether it's a single-day or multi-day event, start,
152
+ and endtime must be set to midnight and be in the same time zone."_
153
+ ([event resource](https://learn.microsoft.com/en-us/graph/api/resources/event)) In practice `end`
154
+ is midnight of the day **after** the last day (so a one-day all-day event ends on the next day's
155
+ midnight) — shown in Microsoft Q&A examples; the API reference states only the midnight + same-zone
156
+ requirement.
157
+ - **isOnlineMeeting**: set true to attach an online (Teams) meeting — _"After you set
158
+ isOnlineMeeting to true, Microsoft Graph initializes onlineMeeting"_ and the provider is
159
+ `teamsForBusiness`. ([event resource](https://learn.microsoft.com/en-us/graph/api/resources/event))
160
+ - **updateEvent attendees REPLACE** the existing list — read current via getEvent, append, then
161
+ update. (PATCH sends only the fields you set.)
162
+ - **deleteEvent** cancels the event; for meetings you organize, attendees are notified
163
+ (Graph exposes a _"Cancel event"_ that _"Send[s] a cancellation message from the organizer to all
164
+ the attendees"_). ([event resource](https://learn.microsoft.com/en-us/graph/api/resources/event))
165
+ - Output enum values are Graph's own: `showAs` ∈ `free, tentative, busy, oof, workingElsewhere,
166
+ unknown`; event `type` ∈ `singleInstance, occurrence, exception, seriesMaster`; `sensitivity` ∈
167
+ `normal, personal, private, confidential`. ([event resource](https://learn.microsoft.com/en-us/graph/api/resources/event))
168
+ Attendee `type` ∈ `required, optional, resource`; the attendee `status.response` is one of
169
+ `none, accepted, declined`, etc. ([attendee](https://learn.microsoft.com/en-us/graph/api/resources/attendee))
170
+
171
+ ## Contacts
172
+
173
+ - A personal contact stores up to **three email addresses** — the resource exposes
174
+ `primaryEmailAddress`, `secondaryEmailAddress`, and `tertiaryEmailAddress`.
175
+ ([contact resource](https://learn.microsoft.com/en-us/graph/api/resources/contact))
176
+ - **updateContact** array fields (`emailAddresses`, `businessPhones`) **replace** existing values —
177
+ read current via getContact and merge.
178
+ - Updating other properties may auto-regenerate `displayName`: _"later updates to other properties
179
+ may cause an automatically generated value to overwrite the displayName value you have specified.
180
+ To preserve a pre-existing value, always include it as displayName."_
181
+ ([contact resource](https://learn.microsoft.com/en-us/graph/api/resources/contact))
182
+
183
+ ## Categories
184
+
185
+ Categories are user-defined names in a **master list**; you apply one by assigning its
186
+ `displayName` to an item's `categories`: _"You can apply a category to an item by assigning the
187
+ displayName property of the category to the categories collection of the item."_ Each category's
188
+ **color** is a preset constant (`None`, `preset0`, `preset1`, … up to 25 colors) that comes from the
189
+ master-list entry. ([outlookCategory](https://learn.microsoft.com/en-us/graph/api/resources/outlookcategory))
190
+ Use **listCategories** to discover the valid names. Categories are per-user (no `mailbox` option).
191
+
192
+ ## Errors, recovery & throttling
193
+
194
+ Graph returns a JSON error envelope: a single `error` object with `code` (machine-readable) and
195
+ `message`. _"You should only code against error codes returned in `code` properties."_
196
+ ([errors](https://learn.microsoft.com/en-us/graph/errors)) Common statuses
197
+ ([HTTP status codes](https://learn.microsoft.com/en-us/graph/errors)):
198
+
199
+ | Status | Meaning | Recovery |
200
+ | ------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
201
+ | `401` | _"Required authentication information is either missing or not valid."_ | Reconnect the Outlook account (token invalid/expired). |
202
+ | `403` | _"Access is denied … The user does not have enough permission or does not have a required license."_ | Reconnect to grant the permission; shared-mailbox access also needs Exchange-side delegation. |
203
+ | `404` | _"The requested resource doesn't exist."_ | The id may be stale — re-fetch it from the relevant list/get tool and retry. |
204
+ | `413` | _"The request size exceeds the maximum limit."_ | Attachment too large — keep inline attachments under 3 MB. |
205
+ | `429` | _"Client application has been throttled."_ | Back off, then retry. |
206
+
207
+ **Throttling (`429`)**: Graph _"Returns HTTP status code 429 Too Many Requests"_ and _"Returns a
208
+ suggested wait time in the response header of the failed request."_ Best practice: _"Wait the number
209
+ of seconds specified in the `Retry-After` header"_ and retry.
210
+ ([throttling](https://learn.microsoft.com/en-us/graph/throttling))
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
3
+ import { z } from "zod";
4
+
5
+ import { connectionResolvers } from "../connections.ts";
6
+ import {
7
+ GRAPH_BASE,
8
+ mailboxRoot,
9
+ outlookFetch,
10
+ parseGraphResponse,
11
+ } from "../lib/graph.ts";
12
+
13
+ const inputSchema = z
14
+ .object({
15
+ messageId: z
16
+ .string()
17
+ .describe(
18
+ "Message id from listMessages or another message tool. Opaque and case-sensitive; changes when the message is moved between folders.",
19
+ ),
20
+ destinationId: z
21
+ .string()
22
+ .describe(
23
+ "Target folder: a folder id from listMailFolders, or a well-known name (inbox, archive, deleteditems, junkemail, drafts, sentitems).",
24
+ ),
25
+ mailbox: z
26
+ .string()
27
+ .describe(
28
+ "Shared-mailbox address (UPN/email) to copy within instead of your own. Requires shared-mailbox delegation. Omit for your own mailbox.",
29
+ )
30
+ .optional(),
31
+ })
32
+ .strict();
33
+
34
+ const outputSchema = z.object({
35
+ id: z.string(),
36
+ parentFolderId: z.string(),
37
+ subject: z.string(),
38
+ });
39
+
40
+ const definition = defineTool({
41
+ name: "copyMessage",
42
+ title: "Copy Message",
43
+ description:
44
+ "Copy a message into another folder, leaving the original in place. Resolve the message id via listMessages first. Returns the new copy, which has its own id distinct from the original.",
45
+ inputSchema,
46
+ outputSchema,
47
+ annotations: {
48
+ readOnlyHint: false,
49
+ destructiveHint: false,
50
+ idempotentHint: false,
51
+ openWorldHint: true,
52
+ },
53
+ connection: "microsoft-outlook",
54
+ run: async (input, ctx) => {
55
+ const url = `${GRAPH_BASE}${mailboxRoot(input.mailbox)}/messages/${encodeURIComponent(
56
+ input.messageId,
57
+ )}/copy`;
58
+ const res = await outlookFetch(ctx.fetch, "copyMessage", url, {
59
+ method: "POST",
60
+ body: JSON.stringify({ destinationId: input.destinationId }),
61
+ });
62
+ return parseGraphResponse(res);
63
+ },
64
+ });
65
+
66
+ export default definition;
67
+
68
+ await handleIfScriptMain(import.meta, definition, { connectionResolvers });
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
3
+ import { z } from "zod";
4
+
5
+ import { connectionResolvers } from "../connections.ts";
6
+ import { GRAPH_BASE, outlookFetch, parseGraphResponse } from "../lib/graph.ts";
7
+ import { contactSchema, outgoingContactSchema } from "../lib/schemas.ts";
8
+
9
+ const inputSchema = z.object({ ...outgoingContactSchema.shape }).strict();
10
+
11
+ const outputSchema = contactSchema;
12
+
13
+ const definition = defineTool({
14
+ name: "createContact",
15
+ title: "Create Contact",
16
+ description:
17
+ "Create a personal contact. No single field is required, but set at least a name (givenName/surname/displayName) or an email address so the contact is identifiable. Microsoft allows a maximum of 3 email addresses per contact.",
18
+ inputSchema,
19
+ outputSchema,
20
+ annotations: {
21
+ readOnlyHint: false,
22
+ destructiveHint: false,
23
+ idempotentHint: false,
24
+ openWorldHint: true,
25
+ },
26
+ connection: "microsoft-outlook",
27
+ run: async (input, ctx) => {
28
+ const url = `${GRAPH_BASE}/me/contacts`;
29
+ const res = await outlookFetch(ctx.fetch, "createContact", url, {
30
+ method: "POST",
31
+ body: JSON.stringify(input),
32
+ });
33
+ return parseGraphResponse(res);
34
+ },
35
+ });
36
+
37
+ export default definition;
38
+
39
+ await handleIfScriptMain(import.meta, definition, { connectionResolvers });