Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ['emailId', 'targetMailboxId'],
},
},
{
name: 'archive_email',
description: 'Archive an email — move it to the target mailbox AND mark it as read in a single atomic JMAP operation. Equivalent to calling move_email followed by mark_email_read, but in one MCP call and one Email/set patch (the move and the read flag land together or not at all). For trashing an email, use delete_email instead — that follows a different convention and does not auto-mark-read.',
inputSchema: {
type: 'object',
properties: {
emailId: {
type: 'string',
description: 'ID of the email to archive',
},
targetMailboxId: {
type: 'string',
description: 'ID of the destination mailbox',
},
},
required: ['emailId', 'targetMailboxId'],
},
},
{
name: 'add_labels',
description: 'Add labels (mailboxes) to an email without removing existing ones',
Expand Down Expand Up @@ -1469,6 +1487,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}

case 'archive_email': {
const { emailId, targetMailboxId } = (args ?? {}) as any;
if (!emailId || !targetMailboxId) {
throw new McpError(ErrorCode.InvalidParams, 'emailId and targetMailboxId are required');
}
const client = initializeClient();
await client.archiveEmail(emailId, targetMailboxId);
return {
content: [
{
type: 'text',
text: 'Email archived successfully (moved to target mailbox and marked as read)',
},
],
};
}

case 'add_labels': {
const { emailId, mailboxIds } = args as any;
if (!emailId) {
Expand Down Expand Up @@ -1753,7 +1788,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
available: true,
functions: [
'list_mailboxes', 'list_emails', 'get_email', 'send_email', 'create_draft', 'edit_draft', 'send_draft', 'search_emails',
'get_recent_emails', 'mark_email_read', 'pin_email', 'delete_email', 'move_email',
'get_recent_emails', 'mark_email_read', 'pin_email', 'delete_email', 'move_email', 'archive_email',
'get_email_attachments', 'download_attachment', 'advanced_search', 'get_thread',
'get_mailbox_stats', 'get_account_summary', 'bulk_mark_read', 'bulk_pin', 'bulk_move', 'bulk_delete',
'add_labels', 'remove_labels', 'bulk_add_labels', 'bulk_remove_labels'
Expand Down
51 changes: 51 additions & 0 deletions src/jmap-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,57 @@ export class JmapClient {
}
}

async archiveEmail(emailId: string, targetMailboxId: string): Promise<void> {
const session = await this.getSession();

const getRequest: JmapRequest = {
using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
methodCalls: [
['Email/get', {
accountId: session.accountId,
ids: [emailId],
properties: ['mailboxIds']
}, 'getEmail']
]
};
const getResponse = await this.makeRequest(getRequest);
const email = this.getListResult(getResponse, 0)[0];

if (!email) {
throw new Error(`Email not found: ${emailId}`);
}

const patch: Record<string, boolean | null> = {};
if (email.mailboxIds) {
for (const mbId of Object.keys(email.mailboxIds)) {
patch[`mailboxIds/${mbId}`] = null;
}
}
patch[`mailboxIds/${targetMailboxId}`] = true;
patch['keywords/$seen'] = true;

const setRequest: JmapRequest = {
using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
methodCalls: [
['Email/set', {
accountId: session.accountId,
update: {
[emailId]: patch
}
}, 'archiveEmail']
]
};

const response = await this.makeRequest(setRequest);
const result = this.getMethodResult(response, 0);

if (result.notUpdated && result.notUpdated[emailId]) {
const err = result.notUpdated[emailId];
const detail = err.description ? ` - ${err.description}` : '';
throw new Error(`Failed to archive email: ${err.type}${detail}`);
}
}

async addLabels(emailId: string, mailboxIds: string[]): Promise<void> {
const session = await this.getSession();

Expand Down