UNPKG

@alexdiazdecerio/fastmail-mcp-server

Version:

🚀 Professional MCP Server for Fastmail email management using JMAP API - Integrates seamlessly with Claude Desktop and other AI assistants

359 lines 14.7 kB
import fetch from 'node-fetch'; export class FastmailClient { email; apiToken; session = null; accountId = null; constructor(email, apiToken) { this.email = email; this.apiToken = apiToken; } async initialize() { console.error('🚀 FASTMAIL CLIENT INITIALIZE - VERSION NUEVA CON LOGS'); // Get session const sessionResponse = await fetch('https://api.fastmail.com/jmap/session', { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!sessionResponse.ok) { throw new Error(`Failed to get session: ${sessionResponse.statusText}`); } this.session = await sessionResponse.json(); // Get primary account ID this.accountId = this.session.primaryAccounts['urn:ietf:params:jmap:mail']; if (!this.accountId) { throw new Error('No primary mail account found'); } } async makeRequest(request) { if (!this.session) { throw new Error('Client not initialized. Call initialize() first.'); } const response = await fetch(this.session.apiUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(request) }); if (!response.ok) { throw new Error(`JMAP request failed: ${response.statusText}`); } return await response.json(); } async getMailboxes() { const response = await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Mailbox/get', { accountId: this.accountId, ids: null }, '0'] ] }); const [, result] = response.methodResponses[0]; return result.list; } async getEmails(options = {}) { // Build filter const filter = {}; if (options.mailboxId) { filter.inMailbox = options.mailboxId; } if (options.filter) { Object.assign(filter, options.filter); // Convert isUnread to notKeyword if ('isUnread' in options.filter) { if (options.filter.isUnread) { filter.notKeyword = '$seen'; } else { filter.hasKeyword = '$seen'; } delete filter.isUnread; } } // First, query for email IDs const queryResponse = await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/query', { accountId: this.accountId, filter, sort: [{ property: 'receivedAt', isAscending: false }], limit: options.limit || 50, position: options.position || 0 }, '0'] ] }); const [, queryResult] = queryResponse.methodResponses[0]; const emailIds = queryResult.ids; const total = queryResult.total; if (emailIds.length === 0) { return { emails: [], total: 0 }; } // Then, get the email details const getResponse = await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/get', { accountId: this.accountId, ids: emailIds, properties: [ 'id', 'blobId', 'threadId', 'mailboxIds', 'keywords', 'size', 'receivedAt', 'subject', 'from', 'to', 'cc', 'bcc', 'replyTo', 'sentAt', 'hasAttachment', 'preview', 'bodyValues', 'textBody', 'htmlBody', 'attachments' ], fetchTextBodyValues: true, fetchHTMLBodyValues: true, maxBodyValueBytes: 256 }, '1'] ] }); const [, getResult] = getResponse.methodResponses[0]; return { emails: getResult.list, total }; } async getEmail(emailId) { const response = await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/get', { accountId: this.accountId, ids: [emailId], properties: null, // Get all properties fetchTextBodyValues: true, fetchHTMLBodyValues: true, maxBodyValueBytes: 100000 // Get more body content }, '0'] ] }); const [, result] = response.methodResponses[0]; return result.list[0] || null; } async sendEmail(options) { console.error('🚀 INICIO sendEmail - Parametros:', JSON.stringify({ to: options.to, cc: options.cc, subject: options.subject, hasTextBody: !!options.textBody }, null, 2)); try { // Get the drafts mailbox ID console.error('🔍 Intentando obtener drafts mailbox...'); const draftsMailboxId = await this.getDraftsMailbox(); console.error('✅ Drafts mailbox obtenido:', draftsMailboxId); // Get the primary identity console.error('🆔 Intentando obtener identidad...'); const identityId = await this.getPrimaryIdentity(); console.error('✅ Identidad obtenida:', identityId); // Create email draft const bodyParts = []; if (options.textBody) { bodyParts.push({ type: 'text/plain', value: options.textBody }); } if (options.htmlBody) { bodyParts.push({ type: 'text/html', value: options.htmlBody }); } const email = { from: [{ email: this.email }], to: options.to, subject: options.subject, keywords: { '$draft': true }, mailboxIds: { [draftsMailboxId]: true }, // Place in drafts mailbox bodyValues: {}, textBody: [], htmlBody: [], attachments: options.attachments || [] }; if (options.cc) email.cc = options.cc; if (options.bcc) email.bcc = options.bcc; if (options.inReplyTo) email.inReplyTo = options.inReplyTo; if (options.references) email.references = options.references; // Add body parts bodyParts.forEach((part, index) => { const partId = `part${index}`; email.bodyValues[partId] = { value: part.value, charset: 'utf-8' }; if (part.type === 'text/plain') { email.textBody.push({ partId, type: 'text/plain' }); } else if (part.type === 'text/html') { email.htmlBody.push({ partId, type: 'text/html' }); } }); // Only include htmlBody if we actually have HTML content if (email.htmlBody.length === 0) { delete email.htmlBody; } // Create draft and send const response = await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail', 'urn:ietf:params:jmap:submission'], methodCalls: [ // Create email draft ['Email/set', { accountId: this.accountId, create: { 'draft': email } }, '0'], // Send the email ['EmailSubmission/set', { accountId: this.accountId, onSuccessDestroyEmail: ['#sendIt'], create: { 'sendIt': { emailId: '#draft', identityId: identityId, envelope: { mailFrom: { email: this.email }, rcptTo: [ ...options.to.map(t => ({ email: t.email })), ...(options.cc || []).map(t => ({ email: t.email })), ...(options.bcc || []).map(t => ({ email: t.email })) ] } } } }, '1'] ] }); console.error('JMAP Response:', JSON.stringify(response, null, 2)); const [, createResult] = response.methodResponses[0]; const [, sendResult] = response.methodResponses[1]; console.error('Create Result:', JSON.stringify(createResult, null, 2)); console.error('Send Result:', JSON.stringify(sendResult, null, 2)); if (!createResult || !createResult.created || !createResult.created.draft) { throw new Error(`Failed to create draft: ${JSON.stringify(createResult)}`); } if (!sendResult || !sendResult.created || !sendResult.created.sendIt) { throw new Error(`Failed to send email: ${JSON.stringify(sendResult)}`); } const emailId = createResult.created.draft.id; const sentAt = sendResult.created.sendIt.sendAt; return { emailId, sentAt }; } catch (error) { console.error('❌ ERROR COMPLETO en sendEmail:', error instanceof Error ? error.message : String(error)); if (error instanceof Error && error.stack) { console.error('Stack trace:', error.stack); } throw error; } } async markAsRead(emailId, read = true) { await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/set', { accountId: this.accountId, update: { [emailId]: { [`keywords/$seen`]: read } } }, '0'] ] }); } async moveEmail(emailId, targetMailboxId) { // First get current mailboxes const email = await this.getEmail(emailId); if (!email) { throw new Error('Email not found'); } // Create new mailboxIds with only the target const newMailboxIds = { [targetMailboxId]: true }; await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/set', { accountId: this.accountId, update: { [emailId]: { mailboxIds: newMailboxIds } } }, '0'] ] }); } async deleteEmail(emailId) { await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/set', { accountId: this.accountId, destroy: [emailId] }, '0'] ] }); } async searchEmails(query, limit = 50) { return this.getEmails({ filter: { text: query }, limit }); } async getDraftsMailbox() { console.error('🔍 Buscando mailbox de drafts...'); try { const mailboxes = await this.getMailboxes(); console.error('📁 Mailboxes encontrados:', JSON.stringify(mailboxes.map(mb => ({ id: mb.id, name: mb.name, role: mb.role })), null, 2)); const draftsMailbox = mailboxes.find(mb => mb.role === 'drafts'); console.error('📝 Drafts mailbox encontrado:', JSON.stringify(draftsMailbox, null, 2)); if (!draftsMailbox) { throw new Error('Drafts mailbox not found'); } console.error('✅ Drafts mailbox ID:', draftsMailbox.id); return draftsMailbox.id; } catch (error) { console.error('❌ Error en getDraftsMailbox:', error instanceof Error ? error.message : String(error)); throw error; } } async getPrimaryIdentity() { console.error('🆔 Buscando identidad principal...'); try { const response = await this.makeRequest({ using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:submission'], methodCalls: [ ['Identity/get', { accountId: this.accountId, ids: null }, '0'] ] }); const [, result] = response.methodResponses[0]; console.error('🆔 Identidades encontradas:', JSON.stringify(result.list, null, 2)); // Use the first identity (usually the primary one) if (!result.list || result.list.length === 0) { throw new Error('No identities found'); } const identityId = result.list[0].id; console.error('✅ Identidad seleccionada:', identityId); return identityId; } catch (error) { console.error('❌ Error en getPrimaryIdentity:', error instanceof Error ? error.message : String(error)); throw error; } } } //# sourceMappingURL=fastmail-client.js.map