m365-cli 0.1.3 → 0.1.4

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.
@@ -7,6 +7,7 @@
7
7
  "https://graph.microsoft.com/Calendars.ReadWrite",
8
8
  "https://graph.microsoft.com/Files.ReadWrite.All",
9
9
  "https://graph.microsoft.com/Sites.ReadWrite.All",
10
+ "https://graph.microsoft.com/User.Read",
10
11
  "offline_access"
11
12
  ],
12
13
  "graphApiUrl": "https://graph.microsoft.com/v1.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m365-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Microsoft 365 CLI - Manage Mail, Calendar, and OneDrive from the command line",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,7 +14,8 @@
14
14
  "m365": "./bin/m365.js"
15
15
  },
16
16
  "scripts": {
17
- "test": "echo \"Error: no test specified\" && exit 1",
17
+ "test": "vitest",
18
+ "test:run": "vitest run",
18
19
  "link": "npm link",
19
20
  "unlink": "npm unlink -g m365-cli"
20
21
  },
@@ -42,5 +43,8 @@
42
43
  },
43
44
  "dependencies": {
44
45
  "commander": "^12.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "vitest": "^4.0.18"
45
49
  }
46
50
  }
@@ -5,6 +5,26 @@ import { outputMailList, outputMailDetail, outputSendResult, outputAttachmentLis
5
5
  import { handleError } from '../utils/error.js';
6
6
  import { isTrustedSender, addTrustedSender, removeTrustedSender, listTrustedSenders, getWhitelistFilePath } from '../utils/trusted-senders.js';
7
7
 
8
+ // Cache for current user's email
9
+ let currentUserEmailCache = null;
10
+ let currentUserEmailWarningShown = false;
11
+
12
+ async function getCurrentUserEmail() {
13
+ if (currentUserEmailCache) return currentUserEmailCache;
14
+
15
+ try {
16
+ const user = await graphClient.getCurrentUser();
17
+ currentUserEmailCache = user.mail || user.userPrincipalName || null;
18
+ return currentUserEmailCache;
19
+ } catch (error) {
20
+ if (!currentUserEmailWarningShown) {
21
+ console.error('Warning: Failed to get current user email (need User.Read permission):', error.message);
22
+ currentUserEmailWarningShown = true;
23
+ }
24
+ return null;
25
+ }
26
+ }
27
+
8
28
  /**
9
29
  * Mail commands
10
30
  */
@@ -18,12 +38,14 @@ export async function listMails(options) {
18
38
 
19
39
  const mails = await graphClient.mail.list({ top, folder });
20
40
 
41
+ const currentUserEmail = await getCurrentUserEmail();
42
+
21
43
  // Mark untrusted senders
22
44
  const mailsWithTrustStatus = mails.map(mail => {
23
45
  const senderEmail = mail.from?.emailAddress?.address;
24
46
  return {
25
47
  ...mail,
26
- isTrusted: senderEmail ? isTrustedSender(senderEmail) : false,
48
+ isTrusted: senderEmail ? isTrustedSender(senderEmail, currentUserEmail) : false,
27
49
  };
28
50
  });
29
51
 
@@ -46,9 +68,11 @@ export async function readMail(id, options) {
46
68
 
47
69
  const mail = await graphClient.mail.get(id);
48
70
 
71
+ const currentUserEmail = await getCurrentUserEmail();
72
+
49
73
  // Check whitelist unless --force is used
50
74
  const senderEmail = mail.from?.emailAddress?.address;
51
- const trusted = senderEmail ? isTrustedSender(senderEmail) : false;
75
+ const trusted = senderEmail ? isTrustedSender(senderEmail, currentUserEmail) : false;
52
76
 
53
77
  if (!trusted && !force) {
54
78
  // Filter content for untrusted senders
@@ -182,12 +206,22 @@ export async function searchMails(query, options) {
182
206
 
183
207
  const mails = await graphClient.mail.search(query, { top });
184
208
 
209
+ const currentUserEmail = await getCurrentUserEmail();
210
+
211
+ const mailsWithTrustStatus = mails.map(mail => {
212
+ const senderEmail = mail.from?.emailAddress?.address;
213
+ return {
214
+ ...mail,
215
+ isTrusted: senderEmail ? isTrustedSender(senderEmail, currentUserEmail) : false,
216
+ };
217
+ });
218
+
185
219
  if (!json) {
186
220
  console.log(`🔍 Search results for: "${query}"`);
187
221
  console.log('');
188
222
  }
189
223
 
190
- outputMailList(mails, { json, top });
224
+ outputMailList(mailsWithTrustStatus, { json, top });
191
225
  } catch (error) {
192
226
  handleError(error, { json: options.json });
193
227
  }
@@ -103,6 +103,21 @@ class GraphClient {
103
103
  async delete(endpoint, options = {}) {
104
104
  return this.request(endpoint, { ...options, method: 'DELETE' });
105
105
  }
106
+
107
+ /**
108
+ * Get current user profile
109
+ */
110
+ async getCurrentUser() {
111
+ return this.get('/me', {
112
+ queryParams: { '$select': 'id,displayName,mail,userPrincipalName' },
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Mail endpoints
118
+ */
119
+ mail = {
120
+ }
106
121
 
107
122
  /**
108
123
  * Mail endpoints
@@ -50,18 +50,27 @@ export function loadTrustedSenders() {
50
50
  /**
51
51
  * Check if a sender is trusted
52
52
  * @param {string} senderEmail - Email address to check
53
+ * @param {string} [currentUserEmail] - Current user's email address (to trust own emails)
53
54
  * @returns {boolean} True if sender is trusted
54
55
  */
55
- export function isTrustedSender(senderEmail) {
56
+ export function isTrustedSender(senderEmail, currentUserEmail) {
56
57
  if (!senderEmail) {
57
58
  return false;
58
59
  }
59
60
 
61
+ // Check if sender is the current user (own emails are trusted)
62
+ if (currentUserEmail) {
63
+ const normalizedSender = senderEmail.toLowerCase().trim();
64
+ const normalizedCurrentUser = currentUserEmail.toLowerCase().trim();
65
+ if (normalizedSender === normalizedCurrentUser) {
66
+ return true;
67
+ }
68
+ }
69
+
60
70
  // Handle Exchange DN format (internal mail)
61
71
  // These are formatted like: /O=EXCHANGELABS/OU=.../CN=RECIPIENTS/CN=...
62
72
  if (senderEmail.startsWith('/O=EXCHANGELABS') || senderEmail.startsWith('/O=EXCHANGE')) {
63
73
  // Internal organization mail - consider trusted
64
- // In a production environment, you might want to be more selective
65
74
  return true;
66
75
  }
67
76
 
@@ -71,7 +80,7 @@ export function isTrustedSender(senderEmail) {
71
80
  for (const entry of trustedSenders) {
72
81
  const normalized = entry.toLowerCase();
73
82
 
74
- // Domain match (e.g., @example.com)
83
+ // Domain match (e.g., @example.com)
75
84
  if (normalized.startsWith('@')) {
76
85
  const domain = normalized.substring(1);
77
86
  if (normalizedEmail.endsWith(`@${domain}`)) {
@@ -117,10 +126,10 @@ export function addTrustedSender(email) {
117
126
  writeFileSync(path, readFileSync(path, 'utf-8') + line, 'utf-8');
118
127
  } else {
119
128
  // Create new file with header
120
- const header = `# M365 Trusted Senders Whitelist\n
121
- # One email address or domain per line\n
122
- # Lines starting with @ match entire domains (e.g. @example.com)\n
123
- # Senders not in this list will have their email body filtered out\n
129
+ const header = `# M365 Trusted Senders Whitelist
130
+ # One email address or domain per line
131
+ # Lines starting with @ match entire domains (e.g. @example.com)
132
+ # Senders not in this list will have their email body filtered out
124
133
 
125
134
  `;
126
135
  writeFileSync(path, header + email + '\n', 'utf-8');