@littlebearapps/outlook-assistant 3.5.2 → 3.7.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/README.md +9 -6
- package/advanced/index.js +5 -5
- package/auth/device-code.js +3 -1
- package/auth/oauth-server.js +4 -4
- package/auth/token-manager.js +2 -2
- package/calendar/list.js +3 -3
- package/categories/index.js +4 -4
- package/contacts/index.js +11 -18
- package/email/conversations.js +19 -17
- package/email/delta.js +1 -1
- package/email/draft.js +482 -0
- package/email/folder-utils.js +1 -1
- package/email/headers.js +10 -10
- package/email/index.js +80 -0
- package/email/list.js +1 -1
- package/email/mime.js +13 -12
- package/email/search.js +3 -2
- package/email/send.js +1 -1
- package/folder/list.js +1 -1
- package/llms.txt +8 -7
- package/package.json +2 -2
- package/rules/create.js +85 -161
- package/rules/index.js +162 -29
- package/rules/list.js +144 -62
- package/rules/rule-builder.js +284 -0
- package/rules/update.js +213 -0
- package/settings/index.js +10 -22
- package/utils/field-presets.js +20 -0
- package/utils/graph-api.js +9 -1
- package/utils/response-formatter.js +8 -6
- package/utils/safety.js +113 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
34
34
|
|
|
35
35
|
- 📨 **Search and read emails** — find messages by sender, subject, date, or keywords; read full threads with conversation grouping; batch flag, move, export, or categorise multiple emails at once
|
|
36
36
|
- 🛡️ **Send emails with safety controls** — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
|
|
37
|
+
- ✏️ **Draft emails for review** — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
|
|
37
38
|
- 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel invitations
|
|
38
39
|
- 📦 **Export emails** — save to Markdown, EML, MBOX, JSON, or HTML for archiving, analysis, or migration; export search results or entire threads in one call
|
|
39
40
|
- 🔍 **Investigate email headers** — check DKIM, SPF, and DMARC authentication; trace delivery chains; analyse spam scores — useful for phishing investigation and compliance
|
|
@@ -60,17 +61,17 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
60
61
|
|
|
61
62
|
| Module | Tools | What You Can Do |
|
|
62
63
|
|--------|------:|-----------------|
|
|
63
|
-
| **Email** |
|
|
64
|
+
| **Email** | 8 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `draft` (create/update/send/delete/reply/forward), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
|
|
64
65
|
| **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (decline/cancel/delete) |
|
|
65
66
|
| **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
|
|
66
67
|
| **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
|
|
67
68
|
| **Settings** | 1 | `mailbox-settings` (get/set auto-replies/set working hours) |
|
|
68
69
|
| **Folder** | 1 | `folders` (list/create/move/stats/delete) |
|
|
69
|
-
| **Rules** | 1 | `manage-rules` (list/create/reorder/delete) |
|
|
70
|
+
| **Rules** | 1 | `manage-rules` (list/create/update/reorder/delete) |
|
|
70
71
|
| **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
|
|
71
72
|
| **Auth** | 1 | `auth` (status/authenticate/about) |
|
|
72
73
|
|
|
73
|
-
**
|
|
74
|
+
**22 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
|
|
74
75
|
|
|
75
76
|
### Export Formats
|
|
76
77
|
|
|
@@ -126,7 +127,9 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
|
|
|
126
127
|
- **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: unlimited)
|
|
127
128
|
- **Recipient allowlist** — restrict sending to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`
|
|
128
129
|
|
|
129
|
-
**
|
|
130
|
+
**Draft protections** — The `draft` tool shares `send-email` safety controls: dry-run preview, recipient allowlist, mail-tips validation, and rate limiting. The `send` action shares the `send-email` rate limit counter, preventing circumvention via the draft-then-send pathway.
|
|
131
|
+
|
|
132
|
+
**Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 22 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
|
|
130
133
|
|
|
131
134
|
> **Important**: These safeguards are defence-in-depth measures that reduce risk, but they are not a guarantee against unintended actions. AI-driven access to your email is inherently sensitive — always review tool calls before approving, particularly for sends and deletes. No automated guardrail is foolproof, and you remain responsible for actions taken through your mailbox.
|
|
132
135
|
|
|
@@ -374,7 +377,7 @@ This starts a local server on port 3333 to handle the OAuth callback.
|
|
|
374
377
|
|
|
375
378
|
```
|
|
376
379
|
outlook-assistant/
|
|
377
|
-
├── index.js # Main entry point (
|
|
380
|
+
├── index.js # Main entry point (22 tools)
|
|
378
381
|
├── config.js # Configuration settings
|
|
379
382
|
├── outlook-auth-server.js # OAuth server (port 3333)
|
|
380
383
|
├── auth/ # Authentication module (1 tool)
|
|
@@ -466,7 +469,7 @@ USE_TEST_MODE=true npm start
|
|
|
466
469
|
| [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
|
|
467
470
|
| [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
|
|
468
471
|
| [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
|
|
469
|
-
| [Tools Reference](docs/quickrefs/tools-reference.md) | All
|
|
472
|
+
| [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
|
|
470
473
|
| [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
|
|
471
474
|
|
|
472
475
|
Full documentation: [docs/](docs/README.md)
|
package/advanced/index.js
CHANGED
|
@@ -96,7 +96,7 @@ async function handleAccessSharedMailbox(args) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
const output = [];
|
|
100
100
|
output.push(`# Shared Mailbox: ${sharedMailbox}`);
|
|
101
101
|
output.push(`**Folder**: ${mailFolder} | **Count**: ${messages.length}\n`);
|
|
102
102
|
|
|
@@ -241,7 +241,7 @@ async function handleSetMessageFlag(args) {
|
|
|
241
241
|
timeZone: DEFAULT_TIMEZONE,
|
|
242
242
|
};
|
|
243
243
|
} else {
|
|
244
|
-
const startOfDay = dueDt.split('T')[0]
|
|
244
|
+
const startOfDay = `${dueDt.split('T')[0]}T09:00:00`;
|
|
245
245
|
flag.startDateTime = {
|
|
246
246
|
dateTime: startOfDay,
|
|
247
247
|
timeZone: DEFAULT_TIMEZONE,
|
|
@@ -269,7 +269,7 @@ async function handleSetMessageFlag(args) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
const output = [];
|
|
273
273
|
|
|
274
274
|
if (results.length > 0) {
|
|
275
275
|
output.push(`Flagged ${results.length} message(s) for follow-up`);
|
|
@@ -377,7 +377,7 @@ async function handleClearMessageFlag(args) {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
const action = markComplete ? 'marked complete' : 'cleared';
|
|
380
|
-
|
|
380
|
+
const output = [];
|
|
381
381
|
|
|
382
382
|
if (results.length > 0) {
|
|
383
383
|
output.push(`${results.length} message(s) ${action}`);
|
|
@@ -504,7 +504,7 @@ async function handleFindMeetingRooms(args) {
|
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
|
|
507
|
+
const output = [];
|
|
508
508
|
output.push(`# Meeting Rooms (${rooms.length})\n`);
|
|
509
509
|
|
|
510
510
|
if (verbosity === 'minimal') {
|
package/auth/device-code.js
CHANGED
|
@@ -92,7 +92,9 @@ async function pollForToken(clientId, deviceCode, interval, expiresIn) {
|
|
|
92
92
|
let pollInterval = interval;
|
|
93
93
|
|
|
94
94
|
while (Date.now() < deadline) {
|
|
95
|
-
await new Promise((resolve) =>
|
|
95
|
+
await new Promise((resolve) => {
|
|
96
|
+
setTimeout(resolve, pollInterval * 1000);
|
|
97
|
+
});
|
|
96
98
|
|
|
97
99
|
const postData = querystring.stringify({
|
|
98
100
|
client_id: clientId,
|
package/auth/oauth-server.js
CHANGED
|
@@ -112,16 +112,16 @@ function setupOAuthRoutes(
|
|
|
112
112
|
// Since this is a module, actual session handling is outside its direct scope,
|
|
113
113
|
// but it's crucial for the consuming application to handle state verification.
|
|
114
114
|
|
|
115
|
-
const authorizationUrl =
|
|
116
|
-
|
|
117
|
-
querystring.stringify({
|
|
115
|
+
const authorizationUrl = `${authConfig.authEndpoint}?${querystring.stringify(
|
|
116
|
+
{
|
|
118
117
|
client_id: authConfig.clientId,
|
|
119
118
|
response_type: 'code',
|
|
120
119
|
redirect_uri: authConfig.redirectUri,
|
|
121
120
|
scope: authConfig.scopes.join(' '),
|
|
122
121
|
response_mode: 'query',
|
|
123
122
|
state: state,
|
|
124
|
-
}
|
|
123
|
+
}
|
|
124
|
+
)}`;
|
|
125
125
|
res.redirect(authorizationUrl);
|
|
126
126
|
});
|
|
127
127
|
|
package/auth/token-manager.js
CHANGED
|
@@ -88,8 +88,8 @@ function getAccessToken() {
|
|
|
88
88
|
*/
|
|
89
89
|
function createTestTokens() {
|
|
90
90
|
const testTokens = {
|
|
91
|
-
access_token:
|
|
92
|
-
refresh_token:
|
|
91
|
+
access_token: `test_access_token_${Date.now()}`,
|
|
92
|
+
refresh_token: `test_refresh_token_${Date.now()}`,
|
|
93
93
|
expires_at: Date.now() + 3600 * 1000, // 1 hour
|
|
94
94
|
};
|
|
95
95
|
|
package/calendar/list.js
CHANGED
|
@@ -18,7 +18,7 @@ async function handleListEvents(args) {
|
|
|
18
18
|
const accessToken = await ensureAuthenticated();
|
|
19
19
|
|
|
20
20
|
// Build API endpoint
|
|
21
|
-
|
|
21
|
+
const endpoint = 'me/events';
|
|
22
22
|
|
|
23
23
|
// Add query parameters
|
|
24
24
|
const queryParams = {
|
|
@@ -54,10 +54,10 @@ async function handleListEvents(args) {
|
|
|
54
54
|
.map((event, index) => {
|
|
55
55
|
const startDt = event.start.dateTime.endsWith('Z')
|
|
56
56
|
? event.start.dateTime
|
|
57
|
-
: event.start.dateTime
|
|
57
|
+
: `${event.start.dateTime}Z`;
|
|
58
58
|
const endDt = event.end.dateTime.endsWith('Z')
|
|
59
59
|
? event.end.dateTime
|
|
60
|
-
: event.end.dateTime
|
|
60
|
+
: `${event.end.dateTime}Z`;
|
|
61
61
|
const startDate = new Date(startDt).toLocaleString('en-AU', {
|
|
62
62
|
timeZone: tz,
|
|
63
63
|
dateStyle: 'medium',
|
package/categories/index.js
CHANGED
|
@@ -106,7 +106,7 @@ async function handleListCategories(args) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// Format output based on verbosity
|
|
109
|
-
|
|
109
|
+
const output = [];
|
|
110
110
|
output.push(`# Master Categories (${categories.length})\n`);
|
|
111
111
|
|
|
112
112
|
if (outputVerbosity === 'minimal') {
|
|
@@ -117,7 +117,7 @@ async function handleListCategories(args) {
|
|
|
117
117
|
categories.forEach((cat) => {
|
|
118
118
|
const colorName = COLOR_NAMES[cat.color] || cat.color;
|
|
119
119
|
const idDisplay =
|
|
120
|
-
outputVerbosity === 'full' ? cat.id : cat.id.substring(0, 8)
|
|
120
|
+
outputVerbosity === 'full' ? cat.id : `${cat.id.substring(0, 8)}...`;
|
|
121
121
|
output.push(`| ${cat.displayName} | ${colorName} | ${idDisplay} |`);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
@@ -503,7 +503,7 @@ async function handleApplyCategory(args) {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
|
|
506
|
+
const output = [];
|
|
507
507
|
|
|
508
508
|
if (results.length > 0) {
|
|
509
509
|
output.push(
|
|
@@ -591,7 +591,7 @@ async function handleGetFocusedInboxOverrides(args) {
|
|
|
591
591
|
};
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
|
|
594
|
+
const output = [];
|
|
595
595
|
output.push(`# Focused Inbox Overrides (${overrides.length})\n`);
|
|
596
596
|
|
|
597
597
|
// Group by classification
|
package/contacts/index.js
CHANGED
|
@@ -66,10 +66,12 @@ function formatContact(contact, verbosity = 'standard') {
|
|
|
66
66
|
// Phone numbers
|
|
67
67
|
const phones = [];
|
|
68
68
|
if (contact.mobilePhone) phones.push(`Mobile: ${contact.mobilePhone}`);
|
|
69
|
-
if (contact.businessPhones?.length > 0)
|
|
69
|
+
if (contact.businessPhones?.length > 0) {
|
|
70
70
|
phones.push(`Work: ${contact.businessPhones[0]}`);
|
|
71
|
-
|
|
71
|
+
}
|
|
72
|
+
if (contact.homePhones?.length > 0) {
|
|
72
73
|
phones.push(`Home: ${contact.homePhones[0]}`);
|
|
74
|
+
}
|
|
73
75
|
if (phones.length > 0) {
|
|
74
76
|
lines.push(`**Phone**: ${phones.join(' | ')}`);
|
|
75
77
|
}
|
|
@@ -86,8 +88,9 @@ function formatContact(contact, verbosity = 'standard') {
|
|
|
86
88
|
// Full verbosity extras
|
|
87
89
|
if (verbosity === 'full') {
|
|
88
90
|
if (contact.department) lines.push(`**Department**: ${contact.department}`);
|
|
89
|
-
if (contact.officeLocation)
|
|
91
|
+
if (contact.officeLocation) {
|
|
90
92
|
lines.push(`**Office**: ${contact.officeLocation}`);
|
|
93
|
+
}
|
|
91
94
|
if (contact.birthday) lines.push(`**Birthday**: ${contact.birthday}`);
|
|
92
95
|
|
|
93
96
|
// Addresses
|
|
@@ -130,12 +133,7 @@ async function handleListContacts(args) {
|
|
|
130
133
|
try {
|
|
131
134
|
const accessToken = await ensureAuthenticated();
|
|
132
135
|
|
|
133
|
-
const fields =
|
|
134
|
-
verbosity === 'full'
|
|
135
|
-
? CONTACT_FIELDS.full
|
|
136
|
-
: verbosity === 'minimal'
|
|
137
|
-
? CONTACT_FIELDS.minimal
|
|
138
|
-
: CONTACT_FIELDS.list;
|
|
136
|
+
const fields = CONTACT_FIELDS[verbosity] || CONTACT_FIELDS.list;
|
|
139
137
|
const endpoint = folder
|
|
140
138
|
? `me/contactFolders/${folder}/contacts`
|
|
141
139
|
: 'me/contacts';
|
|
@@ -155,7 +153,7 @@ async function handleListContacts(args) {
|
|
|
155
153
|
);
|
|
156
154
|
const contacts = response.value || [];
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
const output = [];
|
|
159
157
|
output.push(`# Contacts\n`);
|
|
160
158
|
output.push(`**Total**: ${contacts.length}`);
|
|
161
159
|
output.push('');
|
|
@@ -202,12 +200,7 @@ async function handleSearchContacts(args) {
|
|
|
202
200
|
try {
|
|
203
201
|
const accessToken = await ensureAuthenticated();
|
|
204
202
|
|
|
205
|
-
const fields =
|
|
206
|
-
verbosity === 'full'
|
|
207
|
-
? CONTACT_FIELDS.full
|
|
208
|
-
: verbosity === 'minimal'
|
|
209
|
-
? CONTACT_FIELDS.minimal
|
|
210
|
-
: CONTACT_FIELDS.list;
|
|
203
|
+
const fields = CONTACT_FIELDS[verbosity] || CONTACT_FIELDS.list;
|
|
211
204
|
const endpoint = 'me/contacts';
|
|
212
205
|
|
|
213
206
|
// Build filter for name — emailAddresses/any() lambda is unreliable on personal accounts
|
|
@@ -259,7 +252,7 @@ async function handleSearchContacts(args) {
|
|
|
259
252
|
}
|
|
260
253
|
const contacts = response.value || [];
|
|
261
254
|
|
|
262
|
-
|
|
255
|
+
const output = [];
|
|
263
256
|
output.push(`# Contact Search Results\n`);
|
|
264
257
|
output.push(`**Query**: "${query}"`);
|
|
265
258
|
output.push(`**Found**: ${contacts.length}`);
|
|
@@ -559,7 +552,7 @@ async function handleSearchPeople(args) {
|
|
|
559
552
|
);
|
|
560
553
|
const people = response.value || [];
|
|
561
554
|
|
|
562
|
-
|
|
555
|
+
const output = [];
|
|
563
556
|
output.push(`# People Search Results\n`);
|
|
564
557
|
output.push(`**Query**: "${query}"`);
|
|
565
558
|
output.push(`**Found**: ${people.length} (sorted by relevance)`);
|
package/email/conversations.js
CHANGED
|
@@ -175,12 +175,12 @@ async function handleListConversations(args) {
|
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
// Convert to array and sort by most recent
|
|
178
|
-
|
|
178
|
+
const conversationList = Array.from(conversations.values())
|
|
179
179
|
.sort((a, b) => new Date(b.lastDate) - new Date(a.lastDate))
|
|
180
180
|
.slice(0, count);
|
|
181
181
|
|
|
182
182
|
// Format output
|
|
183
|
-
|
|
183
|
+
const output = [];
|
|
184
184
|
output.push(`# Email Conversations\n`);
|
|
185
185
|
output.push(`**Folder**: ${folder}`);
|
|
186
186
|
output.push(`**Conversations**: ${conversationList.length}`);
|
|
@@ -314,7 +314,7 @@ async function handleGetConversation(args) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// Format output
|
|
317
|
-
|
|
317
|
+
const output = [];
|
|
318
318
|
output.push(`# Email Conversation\n`);
|
|
319
319
|
output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
|
|
320
320
|
output.push(`**Messages**: ${messages.length}`);
|
|
@@ -463,8 +463,8 @@ async function handleExportConversation(args) {
|
|
|
463
463
|
const date = formatDateForFilename(messages[0].receivedDateTime);
|
|
464
464
|
const filenameBase = `${date}_${subject}_conversation`;
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
466
|
+
const exportedFiles = [];
|
|
467
|
+
const exportStats = { messages: messages.length, attachments: 0, bytes: 0 };
|
|
468
468
|
|
|
469
469
|
switch (format) {
|
|
470
470
|
case 'eml': {
|
|
@@ -497,8 +497,8 @@ async function handleExportConversation(args) {
|
|
|
497
497
|
for (const msg of messages) {
|
|
498
498
|
const mimeContent = await callGraphAPIRaw(accessToken, msg.id);
|
|
499
499
|
const from = msg.from?.emailAddress?.address || 'unknown@unknown.com';
|
|
500
|
-
const
|
|
501
|
-
const mboxDate =
|
|
500
|
+
const msgDate = new Date(msg.receivedDateTime);
|
|
501
|
+
const mboxDate = msgDate.toUTCString().replace('GMT', '+0000');
|
|
502
502
|
|
|
503
503
|
// MBOX format: From line + MIME content + blank line
|
|
504
504
|
mboxContent += `From ${from} ${mboxDate}\n`;
|
|
@@ -515,7 +515,7 @@ async function handleExportConversation(args) {
|
|
|
515
515
|
case 'markdown': {
|
|
516
516
|
// Export as threaded Markdown document
|
|
517
517
|
const mdPath = path.join(resolvedDir, `${filenameBase}.md`);
|
|
518
|
-
|
|
518
|
+
const mdContent = [];
|
|
519
519
|
|
|
520
520
|
mdContent.push(
|
|
521
521
|
`# Email Conversation: ${messages[0].subject || '(no subject)'}\n`
|
|
@@ -600,7 +600,7 @@ async function handleExportConversation(args) {
|
|
|
600
600
|
case 'html': {
|
|
601
601
|
// Export as HTML document
|
|
602
602
|
const htmlPath = path.join(resolvedDir, `${filenameBase}.html`);
|
|
603
|
-
|
|
603
|
+
const htmlContent = [];
|
|
604
604
|
|
|
605
605
|
htmlContent.push('<!DOCTYPE html>');
|
|
606
606
|
htmlContent.push('<html><head>');
|
|
@@ -674,14 +674,16 @@ async function handleExportConversation(args) {
|
|
|
674
674
|
}
|
|
675
675
|
|
|
676
676
|
// Format result
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
677
|
+
let sizeFormatted;
|
|
678
|
+
if (exportStats.bytes < 1024) {
|
|
679
|
+
sizeFormatted = `${exportStats.bytes} B`;
|
|
680
|
+
} else if (exportStats.bytes < 1024 * 1024) {
|
|
681
|
+
sizeFormatted = `${(exportStats.bytes / 1024).toFixed(1)} KB`;
|
|
682
|
+
} else {
|
|
683
|
+
sizeFormatted = `${(exportStats.bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const output = [];
|
|
685
687
|
output.push(`# Conversation Exported\n`);
|
|
686
688
|
output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
|
|
687
689
|
output.push(`**Format**: ${format.toUpperCase()}`);
|
package/email/delta.js
CHANGED
|
@@ -88,7 +88,7 @@ async function handleListEmailsDelta(args) {
|
|
|
88
88
|
|
|
89
89
|
// Build response
|
|
90
90
|
const isInitialSync = !deltaToken;
|
|
91
|
-
const hasMoreChanges =
|
|
91
|
+
const hasMoreChanges = Boolean(nextLink);
|
|
92
92
|
const newDeltaToken = deltaLink || nextLink;
|
|
93
93
|
|
|
94
94
|
// Format output based on verbosity
|