@littlebearapps/outlook-assistant 3.12.0 → 3.12.1

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.
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Safe output writes, shared by attachment download, message export and
3
+ * conversation export.
4
+ *
5
+ * Every file the server names itself is written with exclusive create (`wx`),
6
+ * so an existing file is never overwritten and a planted symlink — even a
7
+ * dangling one — is never followed. A clash gets a `-1`, `-2`, … suffix
8
+ * instead, and the result is always confined to `outputDir`.
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const MAX_ATTEMPTS = 1000;
14
+
15
+ /**
16
+ * Like fs.existsSync, but a dangling symlink counts as existing (existsSync
17
+ * follows the link and reports false).
18
+ * @param {string} candidate
19
+ * @returns {boolean}
20
+ */
21
+ function pathEntryExists(candidate) {
22
+ try {
23
+ fs.lstatSync(candidate);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Build `<base>[-N][.ext]` inside `root`, refusing anything that would land
32
+ * outside it. Names are built from sanitised parts, but confine defensively.
33
+ * @param {string} root - Resolved target directory
34
+ * @param {string} base - Name without extension
35
+ * @param {string} ext - Extension with its leading dot, or ''
36
+ * @param {number} suffix - 0 for the plain name, else the collision number
37
+ * @returns {string}
38
+ */
39
+ function candidatePath(root, base, ext, suffix) {
40
+ const name = suffix === 0 ? `${base}${ext}` : `${base}-${suffix}${ext}`;
41
+ const candidate = path.join(root, name);
42
+ if (path.dirname(candidate) !== root) {
43
+ throw new Error('Refusing to write file outside outputDir');
44
+ }
45
+ return candidate;
46
+ }
47
+
48
+ /**
49
+ * Claim a not-yet-used path in `outputDir`: the plain name, else `-1`, `-2`, …
50
+ * until the name is free both on disk and among the paths already claimed in
51
+ * this batch.
52
+ *
53
+ * Silent overwrite is the dangerous part of the batch-export collision defect:
54
+ * the exporter reported `Successful N / Failed 0` while messages vanished.
55
+ * Never overwrite — disambiguate instead, and let the caller reconcile via the
56
+ * paths returned.
57
+ *
58
+ * The claim is synchronous, so it is atomic with respect to the event loop and
59
+ * safe under the batch exporter's 4-way concurrency.
60
+ *
61
+ * @param {string} outputDir - Target directory
62
+ * @param {string} base - Filename without extension
63
+ * @param {string} extension - Extension without a leading dot ('' for none)
64
+ * @param {Set<string>} claimed - Paths already claimed by this batch
65
+ * @returns {string} - An unused absolute path, now claimed
66
+ */
67
+ function claimUniquePath(outputDir, base, extension, claimed) {
68
+ const root = path.resolve(outputDir);
69
+ const ext = extension ? `.${extension}` : '';
70
+ for (let suffix = 0; suffix < MAX_ATTEMPTS; suffix++) {
71
+ const candidate = candidatePath(root, base, ext, suffix);
72
+ if (!claimed.has(candidate) && !pathEntryExists(candidate)) {
73
+ claimed.add(candidate);
74
+ return candidate;
75
+ }
76
+ }
77
+ throw new Error(`Too many files named ${base}${ext} in ${root}`);
78
+ }
79
+
80
+ /**
81
+ * Best-effort removal of a file this call created before its write failed
82
+ * (e.g. ENOSPC/EIO), so no truncated file is left under the claimed name.
83
+ * Only called for non-EEXIST errors: with EEXIST the entry isn't ours.
84
+ * @param {string} candidate
85
+ */
86
+ function removePartialFile(candidate) {
87
+ try {
88
+ fs.unlinkSync(candidate);
89
+ } catch {
90
+ // Already gone (ENOENT) or not removable; the original error matters more.
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Claim a unique name in `outputDir` and write `data` to it exclusively. The
96
+ * `wx` flag fails on any existing entry — including a dangling symlink planted
97
+ * after the claim — so a write never overwrites a file or follows a link; on
98
+ * EEXIST the next suffix is claimed instead. Any other write error removes the
99
+ * partly written file before it is rethrown.
100
+ * @param {string} outputDir - Target directory (must already exist)
101
+ * @param {string} base - Filename without extension (already sanitised)
102
+ * @param {string} extension - Extension without a leading dot ('' for none)
103
+ * @param {Set<string>|null} claimed - Paths already claimed by this batch
104
+ * @param {string|Buffer} data - File contents
105
+ * @param {string} [encoding] - Encoding for string data
106
+ * @returns {string} - Absolute path actually written
107
+ */
108
+ function writeClaimedFile(outputDir, base, extension, claimed, data, encoding) {
109
+ const seen = claimed || new Set();
110
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
111
+ const candidate = claimUniquePath(outputDir, base, extension, seen);
112
+ try {
113
+ fs.writeFileSync(candidate, data, { encoding, flag: 'wx' });
114
+ return candidate;
115
+ } catch (error) {
116
+ if (error.code !== 'EEXIST') {
117
+ removePartialFile(candidate);
118
+ throw error;
119
+ }
120
+ }
121
+ }
122
+ throw new Error(`Too many files named ${base} in ${outputDir}`);
123
+ }
124
+
125
+ /**
126
+ * Create a new, empty directory `<base>` (or `<base>-N`) inside `outputDir`.
127
+ * A non-recursive mkdir fails on any existing entry, so an export never writes
128
+ * into a directory it didn't just create — including a symlink pointing out of
129
+ * `outputDir`.
130
+ * @param {string} outputDir - Parent directory (must already exist)
131
+ * @param {string} base - Directory name (already sanitised)
132
+ * @returns {string} - Absolute path of the directory created
133
+ */
134
+ function makeClaimedDir(outputDir, base) {
135
+ const root = path.resolve(outputDir);
136
+ for (let suffix = 0; suffix < MAX_ATTEMPTS; suffix++) {
137
+ const candidate = candidatePath(root, base, '', suffix);
138
+ try {
139
+ fs.mkdirSync(candidate);
140
+ return candidate;
141
+ } catch (error) {
142
+ if (error.code !== 'EEXIST') throw error;
143
+ }
144
+ }
145
+ throw new Error(`Too many directories named ${base} in ${root}`);
146
+ }
147
+
148
+ module.exports = {
149
+ writeClaimedFile,
150
+ makeClaimedDir,
151
+ };
@@ -1,72 +0,0 @@
1
- /**
2
- * Accept event functionality
3
- */
4
- const { callGraphAPI } = require('../utils/graph-api');
5
- const { ensureAuthenticated } = require('../auth');
6
-
7
- /**
8
- * Accept event handler
9
- * @param {object} args - Tool arguments
10
- * @returns {object} - MCP response
11
- */
12
- async function handleAcceptEvent(args) {
13
- const { eventId, comment } = args;
14
-
15
- if (!eventId) {
16
- return {
17
- content: [
18
- {
19
- type: 'text',
20
- text: 'Event ID is required to accept an event.',
21
- },
22
- ],
23
- };
24
- }
25
-
26
- try {
27
- // Get access token
28
- const accessToken = await ensureAuthenticated();
29
-
30
- // Build API endpoint
31
- const endpoint = `me/events/${eventId}/accept`;
32
-
33
- // Request body
34
- const body = {
35
- comment: comment || 'Accepted via API',
36
- };
37
-
38
- // Make API call
39
- await callGraphAPI(accessToken, 'POST', endpoint, body);
40
-
41
- return {
42
- content: [
43
- {
44
- type: 'text',
45
- text: `Event with ID ${eventId} has been successfully accepted.`,
46
- },
47
- ],
48
- };
49
- } catch (error) {
50
- if (error.message === 'Authentication required') {
51
- return {
52
- content: [
53
- {
54
- type: 'text',
55
- text: "Authentication required. Please use the 'authenticate' tool first.",
56
- },
57
- ],
58
- };
59
- }
60
-
61
- return {
62
- content: [
63
- {
64
- type: 'text',
65
- text: `Error accepting event: ${error.message}`,
66
- },
67
- ],
68
- };
69
- }
70
- }
71
-
72
- module.exports = handleAcceptEvent;