outlook-cli 1.2.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.
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ const http = require('http');
3
+ const url = require('url');
4
+ const querystring = require('querystring');
5
+ const https = require('https');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const config = require('./config');
9
+
10
+ // Load environment variables from .env file
11
+ require('dotenv').config();
12
+
13
+ // Log to console
14
+ console.log('Starting Outlook Authentication Server');
15
+
16
+ // Authentication configuration
17
+ const AUTH_CONFIG = {
18
+ clientId: config.AUTH_CONFIG.clientId,
19
+ clientSecret: config.AUTH_CONFIG.clientSecret,
20
+ redirectUri: config.AUTH_CONFIG.redirectUri,
21
+ scopes: Array.from(new Set(['offline_access', ...config.AUTH_CONFIG.scopes, 'Contacts.Read'])),
22
+ tokenStorePath: config.AUTH_CONFIG.tokenStorePath || path.join(process.env.HOME || process.env.USERPROFILE, '.outlook-mcp-tokens.json')
23
+ };
24
+
25
+ // Create HTTP server
26
+ const server = http.createServer((req, res) => {
27
+ const parsedUrl = url.parse(req.url, true);
28
+ const pathname = parsedUrl.pathname;
29
+
30
+ console.log(`Request received: ${pathname}`);
31
+
32
+ if (pathname === '/auth/callback') {
33
+ const query = parsedUrl.query;
34
+
35
+ if (query.error) {
36
+ console.error(`Authentication error: ${query.error} - ${query.error_description}`);
37
+ res.writeHead(400, { 'Content-Type': 'text/html' });
38
+ res.end(`
39
+ <html>
40
+ <head>
41
+ <title>Authentication Error</title>
42
+ <style>
43
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
44
+ h1 { color: #d9534f; }
45
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <h1>Authentication Error</h1>
50
+ <div class="error-box">
51
+ <p><strong>Error:</strong> ${query.error}</p>
52
+ <p><strong>Description:</strong> ${query.error_description || 'No description provided'}</p>
53
+ </div>
54
+ <p>Please close this window and try again.</p>
55
+ </body>
56
+ </html>
57
+ `);
58
+ return;
59
+ }
60
+
61
+ if (query.code) {
62
+ console.log('Authorization code received, exchanging for tokens...');
63
+
64
+ // Exchange code for tokens
65
+ exchangeCodeForTokens(query.code)
66
+ .then((tokens) => {
67
+ console.log('Token exchange successful');
68
+ res.writeHead(200, { 'Content-Type': 'text/html' });
69
+ res.end(`
70
+ <html>
71
+ <head>
72
+ <title>Authentication Successful</title>
73
+ <style>
74
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
75
+ h1 { color: #5cb85c; }
76
+ .success-box { background-color: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 4px; }
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <h1>Authentication Successful!</h1>
81
+ <div class="success-box">
82
+ <p>You have successfully authenticated with Microsoft Graph API.</p>
83
+ <p>The access token has been saved securely.</p>
84
+ </div>
85
+ <p>You can now close this window and return to Claude.</p>
86
+ </body>
87
+ </html>
88
+ `);
89
+ })
90
+ .catch((error) => {
91
+ console.error(`Token exchange error: ${error.message}`);
92
+ res.writeHead(500, { 'Content-Type': 'text/html' });
93
+ res.end(`
94
+ <html>
95
+ <head>
96
+ <title>Token Exchange Error</title>
97
+ <style>
98
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
99
+ h1 { color: #d9534f; }
100
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <h1>Token Exchange Error</h1>
105
+ <div class="error-box">
106
+ <p>${error.message}</p>
107
+ </div>
108
+ <p>Please close this window and try again.</p>
109
+ </body>
110
+ </html>
111
+ `);
112
+ });
113
+ } else {
114
+ console.error('No authorization code provided');
115
+ res.writeHead(400, { 'Content-Type': 'text/html' });
116
+ res.end(`
117
+ <html>
118
+ <head>
119
+ <title>Missing Authorization Code</title>
120
+ <style>
121
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
122
+ h1 { color: #d9534f; }
123
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <h1>Missing Authorization Code</h1>
128
+ <div class="error-box">
129
+ <p>No authorization code was provided in the callback.</p>
130
+ </div>
131
+ <p>Please close this window and try again.</p>
132
+ </body>
133
+ </html>
134
+ `);
135
+ }
136
+ } else if (pathname === '/auth') {
137
+ // Handle the /auth route - redirect to Microsoft's OAuth authorization endpoint
138
+ console.log('Auth request received, redirecting to Microsoft login...');
139
+
140
+ // Verify credentials are set
141
+ if (!AUTH_CONFIG.clientId || !AUTH_CONFIG.clientSecret) {
142
+ res.writeHead(500, { 'Content-Type': 'text/html' });
143
+ res.end(`
144
+ <html>
145
+ <head>
146
+ <title>Configuration Error</title>
147
+ <style>
148
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
149
+ h1 { color: #d9534f; }
150
+ .error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 4px; }
151
+ code { background: #f4f4f4; padding: 2px 4px; border-radius: 4px; }
152
+ </style>
153
+ </head>
154
+ <body>
155
+ <h1>Configuration Error</h1>
156
+ <div class="error-box">
157
+ <p>Microsoft Graph API credentials are not set. Please set the following environment variables:</p>
158
+ <ul>
159
+ <li><code>OUTLOOK_CLIENT_ID</code> or <code>MS_CLIENT_ID</code></li>
160
+ <li><code>OUTLOOK_CLIENT_SECRET</code> or <code>MS_CLIENT_SECRET</code></li>
161
+ </ul>
162
+ </div>
163
+ </body>
164
+ </html>
165
+ `);
166
+ return;
167
+ }
168
+
169
+ // Get client_id from query parameters or use the default
170
+ const query = parsedUrl.query;
171
+ const clientId = query.client_id || AUTH_CONFIG.clientId;
172
+
173
+ // Build the authorization URL
174
+ const authParams = {
175
+ client_id: clientId,
176
+ response_type: 'code',
177
+ redirect_uri: AUTH_CONFIG.redirectUri,
178
+ scope: AUTH_CONFIG.scopes.join(' '),
179
+ response_mode: 'query',
180
+ state: Date.now().toString() // Simple state parameter for security
181
+ };
182
+
183
+ const authUrl = `https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?${querystring.stringify(authParams)}`;
184
+ console.log(`Redirecting to: ${authUrl}`);
185
+
186
+ // Redirect to Microsoft's login page
187
+ res.writeHead(302, { 'Location': authUrl });
188
+ res.end();
189
+ } else if (pathname === '/') {
190
+ // Root path - provide instructions
191
+ res.writeHead(200, { 'Content-Type': 'text/html' });
192
+ res.end(`
193
+ <html>
194
+ <head>
195
+ <title>Outlook Authentication Server</title>
196
+ <style>
197
+ body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
198
+ h1 { color: #0078d4; }
199
+ .info-box { background-color: #e7f6fd; border: 1px solid #b3e0ff; padding: 15px; border-radius: 4px; }
200
+ code { background: #f4f4f4; padding: 2px 4px; border-radius: 4px; }
201
+ </style>
202
+ </head>
203
+ <body>
204
+ <h1>Outlook Authentication Server</h1>
205
+ <div class="info-box">
206
+ <p>This server is running to handle Microsoft Graph API authentication callbacks.</p>
207
+ <p>Don't navigate here directly. Instead, use the <code>authenticate</code> tool in Claude to start the authentication process.</p>
208
+ <p>Make sure you've set <code>OUTLOOK_CLIENT_ID</code>/<code>OUTLOOK_CLIENT_SECRET</code> or <code>MS_CLIENT_ID</code>/<code>MS_CLIENT_SECRET</code>.</p>
209
+ </div>
210
+ <p>Server is running at http://localhost:3333</p>
211
+ </body>
212
+ </html>
213
+ `);
214
+ } else {
215
+ // Not found
216
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
217
+ res.end('Not Found');
218
+ }
219
+ });
220
+
221
+ function exchangeCodeForTokens(code) {
222
+ return new Promise((resolve, reject) => {
223
+ const postData = querystring.stringify({
224
+ client_id: AUTH_CONFIG.clientId,
225
+ client_secret: AUTH_CONFIG.clientSecret,
226
+ code: code,
227
+ redirect_uri: AUTH_CONFIG.redirectUri,
228
+ grant_type: 'authorization_code',
229
+ scope: AUTH_CONFIG.scopes.join(' ')
230
+ });
231
+
232
+ const options = {
233
+ hostname: 'login.microsoftonline.com',
234
+ path: '/consumers/oauth2/v2.0/token',
235
+ method: 'POST',
236
+ headers: {
237
+ 'Content-Type': 'application/x-www-form-urlencoded',
238
+ 'Content-Length': Buffer.byteLength(postData)
239
+ }
240
+ };
241
+
242
+ const req = https.request(options, (res) => {
243
+ let data = '';
244
+
245
+ res.on('data', (chunk) => {
246
+ data += chunk;
247
+ });
248
+
249
+ res.on('end', () => {
250
+ if (res.statusCode >= 200 && res.statusCode < 300) {
251
+ try {
252
+ const tokenResponse = JSON.parse(data);
253
+
254
+ // Calculate expiration time (current time + expires_in seconds)
255
+ const expiresAt = Date.now() + (tokenResponse.expires_in * 1000);
256
+
257
+ // Add expires_at for easier expiration checking
258
+ tokenResponse.expires_at = expiresAt;
259
+
260
+ // Save tokens to file
261
+ fs.writeFileSync(AUTH_CONFIG.tokenStorePath, JSON.stringify(tokenResponse, null, 2), 'utf8');
262
+ console.log(`Tokens saved to ${AUTH_CONFIG.tokenStorePath}`);
263
+
264
+ resolve(tokenResponse);
265
+ } catch (error) {
266
+ reject(new Error(`Error parsing token response: ${error.message}`));
267
+ }
268
+ } else {
269
+ reject(new Error(`Token exchange failed with status ${res.statusCode}: ${data}`));
270
+ }
271
+ });
272
+ });
273
+
274
+ req.on('error', (error) => {
275
+ reject(error);
276
+ });
277
+
278
+ req.write(postData);
279
+ req.end();
280
+ });
281
+ }
282
+
283
+ // Start server
284
+ const PORT = 3333;
285
+ server.listen(PORT, () => {
286
+ console.log(`Authentication server running at http://localhost:${PORT}`);
287
+ console.log(`Waiting for authentication callback at ${AUTH_CONFIG.redirectUri}`);
288
+ console.log(`Token will be stored at: ${AUTH_CONFIG.tokenStorePath}`);
289
+
290
+ if (!AUTH_CONFIG.clientId || !AUTH_CONFIG.clientSecret) {
291
+ console.log('\n⚠️ WARNING: Microsoft Graph API credentials are not set.');
292
+ console.log(' Please set OUTLOOK_CLIENT_ID/OUTLOOK_CLIENT_SECRET or MS_CLIENT_ID/MS_CLIENT_SECRET.');
293
+ }
294
+ });
295
+
296
+ // Handle termination
297
+ process.on('SIGINT', () => {
298
+ console.log('Authentication server shutting down');
299
+ process.exit(0);
300
+ });
301
+
302
+ process.on('SIGTERM', () => {
303
+ console.log('Authentication server shutting down');
304
+ process.exit(0);
305
+ });
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "outlook-cli",
3
+ "version": "1.2.0",
4
+ "description": "Production-ready Outlook CLI with optional MCP server mode powered by Microsoft Graph API",
5
+ "keywords": [
6
+ "outlook-cli",
7
+ "cli",
8
+ "claude",
9
+ "outlook",
10
+ "mcp",
11
+ "microsoft-graph",
12
+ "email"
13
+ ],
14
+ "homepage": "https://github.com/selvin-paul-raj/outlook-cli#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/selvin-paul-raj/outlook-cli/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/selvin-paul-raj/outlook-cli.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Selvin PaulRaj K",
24
+ "type": "commonjs",
25
+ "main": "index.js",
26
+ "bin": {
27
+ "outlook-cli": "cli.js"
28
+ },
29
+ "directories": {
30
+ "doc": "docs",
31
+ "test": "test"
32
+ },
33
+ "files": [
34
+ "auth",
35
+ "calendar",
36
+ "email",
37
+ "folder",
38
+ "rules",
39
+ "utils",
40
+ "cli.js",
41
+ "index.js",
42
+ "tool-registry.js",
43
+ "config.js",
44
+ "outlook-auth-server.js",
45
+ "README.md",
46
+ "CLI.md",
47
+ "QUICKSTART.md",
48
+ "docs",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "start": "node cli.js",
53
+ "mcp-server": "node index.js",
54
+ "cli": "node cli.js",
55
+ "doctor": "node cli.js doctor",
56
+ "auth-server": "node outlook-auth-server.js",
57
+ "test-mode": "node -e \"process.env.USE_TEST_MODE='true'; require('./index.js');\"",
58
+ "inspect": "npx @modelcontextprotocol/inspector node index.js",
59
+ "test": "jest",
60
+ "prepublishOnly": "npm test && npm pack --dry-run"
61
+ },
62
+ "dependencies": {
63
+ "@modelcontextprotocol/sdk": "^1.1.0",
64
+ "chalk": "^4.1.2",
65
+ "dotenv": "^16.5.0"
66
+ },
67
+ "devDependencies": {
68
+ "@modelcontextprotocol/inspector": "^0.10.2",
69
+ "jest": "^29.7.0",
70
+ "supertest": "^7.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=18.0.0"
74
+ },
75
+ "preferGlobal": true
76
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Create rule functionality
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+ const { ensureAuthenticated } = require('../auth');
6
+ const { getFolderIdByName } = require('../email/folder-utils');
7
+ const { getInboxRules } = require('./list');
8
+
9
+ /**
10
+ * Create rule handler
11
+ * @param {object} args - Tool arguments
12
+ * @returns {object} - MCP response
13
+ */
14
+ async function handleCreateRule(args) {
15
+ const {
16
+ name,
17
+ fromAddresses,
18
+ containsSubject,
19
+ hasAttachments,
20
+ moveToFolder,
21
+ markAsRead,
22
+ isEnabled = true,
23
+ sequence
24
+ } = args;
25
+
26
+ // Add validation for sequence parameter
27
+ if (sequence !== undefined && (isNaN(sequence) || sequence < 1)) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: "Sequence must be a positive number greater than zero."
32
+ }]
33
+ };
34
+ }
35
+
36
+ if (!name) {
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: "Rule name is required."
41
+ }]
42
+ };
43
+ }
44
+
45
+ // Validate that at least one condition or action is specified
46
+ const hasCondition = fromAddresses || containsSubject || hasAttachments === true;
47
+ const hasAction = moveToFolder || markAsRead === true;
48
+
49
+ if (!hasCondition) {
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: "At least one condition is required. Specify fromAddresses, containsSubject, or hasAttachments."
54
+ }]
55
+ };
56
+ }
57
+
58
+ if (!hasAction) {
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: "At least one action is required. Specify moveToFolder or markAsRead."
63
+ }]
64
+ };
65
+ }
66
+
67
+ try {
68
+ // Get access token
69
+ const accessToken = await ensureAuthenticated();
70
+
71
+ // Create rule
72
+ const result = await createInboxRule(accessToken, {
73
+ name,
74
+ fromAddresses,
75
+ containsSubject,
76
+ hasAttachments,
77
+ moveToFolder,
78
+ markAsRead,
79
+ isEnabled,
80
+ sequence
81
+ });
82
+
83
+ let responseText = result.message;
84
+
85
+ // Add a tip about sequence if it wasn't provided
86
+ if (!sequence && !result.error) {
87
+ responseText += "\n\nTip: You can specify a 'sequence' parameter when creating rules to control their execution order. Lower sequence numbers run first.";
88
+ }
89
+
90
+ return {
91
+ content: [{
92
+ type: "text",
93
+ text: responseText
94
+ }]
95
+ };
96
+ } catch (error) {
97
+ if (error.message === 'Authentication required') {
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: "Authentication required. Please use the 'authenticate' tool first."
102
+ }]
103
+ };
104
+ }
105
+
106
+ return {
107
+ content: [{
108
+ type: "text",
109
+ text: `Error creating rule: ${error.message}`
110
+ }]
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Create a new inbox rule
117
+ * @param {string} accessToken - Access token
118
+ * @param {object} ruleOptions - Rule creation options
119
+ * @returns {Promise<object>} - Result object with status and message
120
+ */
121
+ async function createInboxRule(accessToken, ruleOptions) {
122
+ try {
123
+ const {
124
+ name,
125
+ fromAddresses,
126
+ containsSubject,
127
+ hasAttachments,
128
+ moveToFolder,
129
+ markAsRead,
130
+ isEnabled,
131
+ sequence
132
+ } = ruleOptions;
133
+
134
+ // Get existing rules to determine sequence if not provided
135
+ let ruleSequence = sequence;
136
+ if (!ruleSequence) {
137
+ try {
138
+ // Default to 100 if we can't get existing rules
139
+ ruleSequence = 100;
140
+
141
+ // Get existing rules to find highest sequence
142
+ const existingRules = await getInboxRules(accessToken);
143
+ if (existingRules && existingRules.length > 0) {
144
+ // Find the highest sequence
145
+ const highestSequence = Math.max(...existingRules.map(r => r.sequence || 0));
146
+ // Set new rule sequence to be higher
147
+ ruleSequence = Math.max(highestSequence + 1, 100);
148
+ console.error(`Auto-generated sequence: ${ruleSequence} (based on highest existing: ${highestSequence})`);
149
+ }
150
+ } catch (sequenceError) {
151
+ console.error(`Error determining rule sequence: ${sequenceError.message}`);
152
+ // Fall back to default value
153
+ ruleSequence = 100;
154
+ }
155
+ }
156
+
157
+ console.error(`Using rule sequence: ${ruleSequence}`);
158
+
159
+ // Make sure sequence is a positive integer
160
+ ruleSequence = Math.max(1, Math.floor(ruleSequence));
161
+
162
+ // Build rule object with sequence
163
+ const rule = {
164
+ displayName: name,
165
+ isEnabled: isEnabled === true,
166
+ sequence: ruleSequence,
167
+ conditions: {},
168
+ actions: {}
169
+ };
170
+
171
+ // Add conditions
172
+ if (fromAddresses) {
173
+ // Parse email addresses
174
+ const emailAddresses = fromAddresses.split(',')
175
+ .map(email => email.trim())
176
+ .filter(email => email)
177
+ .map(email => ({
178
+ emailAddress: {
179
+ address: email
180
+ }
181
+ }));
182
+
183
+ if (emailAddresses.length > 0) {
184
+ rule.conditions.fromAddresses = emailAddresses;
185
+ }
186
+ }
187
+
188
+ if (containsSubject) {
189
+ rule.conditions.subjectContains = [containsSubject];
190
+ }
191
+
192
+ if (hasAttachments === true) {
193
+ rule.conditions.hasAttachment = true;
194
+ }
195
+
196
+ // Add actions
197
+ if (moveToFolder) {
198
+ // Get folder ID
199
+ try {
200
+ const folderId = await getFolderIdByName(accessToken, moveToFolder);
201
+ if (!folderId) {
202
+ return {
203
+ success: false,
204
+ message: `Target folder "${moveToFolder}" not found. Please specify a valid folder name.`
205
+ };
206
+ }
207
+
208
+ rule.actions.moveToFolder = folderId;
209
+ } catch (folderError) {
210
+ console.error(`Error resolving folder "${moveToFolder}": ${folderError.message}`);
211
+ return {
212
+ success: false,
213
+ message: `Error resolving folder "${moveToFolder}": ${folderError.message}`
214
+ };
215
+ }
216
+ }
217
+
218
+ if (markAsRead === true) {
219
+ rule.actions.markAsRead = true;
220
+ }
221
+
222
+ // Create the rule
223
+ const response = await callGraphAPI(
224
+ accessToken,
225
+ 'POST',
226
+ 'me/mailFolders/inbox/messageRules',
227
+ rule
228
+ );
229
+
230
+ if (response && response.id) {
231
+ return {
232
+ success: true,
233
+ message: `Successfully created rule "${name}" with sequence ${ruleSequence}.`,
234
+ ruleId: response.id
235
+ };
236
+ } else {
237
+ return {
238
+ success: false,
239
+ message: "Failed to create rule. The server didn't return a rule ID."
240
+ };
241
+ }
242
+ } catch (error) {
243
+ console.error(`Error creating rule: ${error.message}`);
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ module.exports = handleCreateRule;