send_draft
Send a draft email using its ID. The draft must include recipients and a from address. After sending, the email is moved to the Sent folder and the draft keyword is removed.
Instructions
Send an existing draft email. The draft must have recipients (to/cc/bcc) and a from address. After sending, the email is moved to the Sent folder and the draft keyword is removed.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| emailId | Yes | The ID of the draft email to send |
Implementation Reference
- src/index.ts:399-412 (schema)Input schema definition for the send_draft tool. Accepts a single required parameter: emailId (string).
{ name: 'send_draft', description: 'Send an existing draft email. The draft must have recipients (to/cc/bcc) and a from address. After sending, the email is moved to the Sent folder and the draft keyword is removed.', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'The ID of the draft email to send', }, }, required: ['emailId'], }, }, - src/index.ts:1219-1235 (handler)Handler for the 'send_draft' tool call. Extracts emailId from args, validates it, calls client.sendDraft(emailId), and returns the submission ID.
case 'send_draft': { const { emailId } = args as any; if (!emailId) { throw new McpError(ErrorCode.InvalidParams, 'emailId is required'); } const submissionId = await client.sendDraft(emailId); return { content: [ { type: 'text', text: `Draft sent successfully. Submission ID: ${submissionId}`, }, ], }; } - src/jmap-client.ts:615-713 (helper)Implementation of sendDraft() on JmapClient. Fetches the draft email, verifies it has $draft keyword and has recipients/from, resolves identity, finds Sent mailbox, and submits via EmailSubmission/set with onSuccessUpdateEmail to move to Sent folder and remove $draft keyword.
async sendDraft(emailId: string): Promise<string> { const session = await this.getSession(); // Fetch the existing email to verify it's a draft const getRequest: JmapRequest = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/get', { accountId: session.accountId, ids: [emailId], properties: ['id', 'from', 'to', 'cc', 'bcc', 'replyTo', 'keywords'], }, 'getEmail'] ] }; const getResponse = await this.makeRequest(getRequest); const email = this.getListResult(getResponse, 0)[0]; if (!email) { throw new Error(`Email with ID '${emailId}' not found`); } if (!email.keywords?.$draft) { throw new Error('Cannot send a non-draft email'); } // Collect all recipients for the envelope const allRecipients: { email: string }[] = [ ...(email.to || []), ...(email.cc || []), ...(email.bcc || []), ]; if (allRecipients.length === 0) { throw new Error('Draft has no recipients'); } // Determine identity from the email's from field const fromEmail = email.from?.[0]?.email; if (!fromEmail) { throw new Error('Draft has no from address'); } const identities = await this.getIdentities(); const selectedIdentity = identities.find(id => matchesIdentity(id.email, fromEmail)); if (!selectedIdentity) { throw new Error('From address on draft does not match any sending identity'); } // Find the Sent mailbox const mailboxes = await this.getMailboxes(); const sentMailbox = this.findMailboxByRoleOrName(mailboxes, 'sent', 'sent'); if (!sentMailbox) { throw new Error('Could not find Sent mailbox'); } const sentMailboxIds: Record<string, boolean> = {}; sentMailboxIds[sentMailbox.id] = true; // Submit the draft const request: JmapRequest = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail', 'urn:ietf:params:jmap:submission'], methodCalls: [ ['EmailSubmission/set', { accountId: session.accountId, create: { submission: { emailId, identityId: selectedIdentity.id, envelope: { mailFrom: { email: fromEmail }, rcptTo: allRecipients.map(addr => ({ email: addr.email })), } } }, onSuccessUpdateEmail: { '#submission': { mailboxIds: sentMailboxIds, 'keywords/$draft': null, 'keywords/$seen': true, } } }, 'submitDraft'] ] }; const response = await this.makeRequest(request); const submissionResult = this.getMethodResult(response, 0); if (submissionResult.notCreated?.submission) { const err = submissionResult.notCreated.submission; throw new Error(`Failed to submit draft: ${err.type}${err.description ? ' - ' + err.description : ''}`); } const submissionId = submissionResult.created?.submission?.id; if (!submissionId) { throw new Error('Draft submission returned no submission ID'); } return submissionId; } - src/index.ts:131-987 (registration)ListToolsRequestSchema handler where the send_draft tool is registered in the tools array (lines 399-412) alongside all other tools.
server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'list_mailboxes', description: 'List all mailboxes in the Fastmail account', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_emails', description: 'List emails from a mailbox', inputSchema: { type: 'object', properties: { mailboxId: { type: 'string', description: 'ID of the mailbox to list emails from (optional, defaults to all)', }, limit: { type: ['number', 'string'], description: 'Maximum number of emails to return (default: 20)', default: 20, }, ascending: { type: 'boolean', description: 'Sort oldest first instead of newest first (default: false)', }, }, }, }, { name: 'get_email', description: 'Get a specific email by ID', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to retrieve', }, }, required: ['emailId'], }, }, { name: 'send_email', description: 'Send an email', inputSchema: { type: 'object', properties: { to: { oneOf: [ { type: 'array', items: { type: 'string' } }, { type: 'string' }, ], description: 'Recipient email addresses (array of strings, or a comma-separated string)', }, cc: { type: 'array', items: { type: 'string' }, description: 'CC email addresses (optional)', }, bcc: { type: 'array', items: { type: 'string' }, description: 'BCC email addresses (optional)', }, from: { type: 'string', description: 'Sender email address (optional, defaults to account primary email)', }, mailboxId: { type: 'string', description: 'Mailbox ID to save the email to (optional, defaults to Drafts folder)', }, subject: { type: 'string', description: 'Email subject', }, textBody: { type: 'string', description: 'Plain text body (optional)', }, htmlBody: { type: 'string', description: 'HTML body (optional)', }, inReplyTo: { type: 'array', items: { type: 'string' }, description: 'Message-ID(s) of the email being replied to (optional, for threading)', }, references: { type: 'array', items: { type: 'string' }, description: 'Full reference chain of Message-IDs (optional, for threading)', }, replyTo: { type: 'array', items: { type: 'string' }, description: 'Reply-To email addresses (replies go here instead of to the sender)', }, }, required: ['to', 'subject'], }, }, { name: 'reply_email', description: 'Reply to an existing email with proper threading headers (In-Reply-To, References). Automatically fetches the original email to build the reply chain. By default sends immediately; set send=false to save as a draft instead.', inputSchema: { type: 'object', properties: { originalEmailId: { type: 'string', description: 'ID of the email to reply to', }, to: { type: 'array', items: { type: 'string' }, description: 'Recipient email addresses (optional, defaults to the original sender)', }, cc: { type: 'array', items: { type: 'string' }, description: 'CC email addresses (optional)', }, bcc: { type: 'array', items: { type: 'string' }, description: 'BCC email addresses (optional)', }, from: { type: 'string', description: 'Sender email address (optional, defaults to account primary email)', }, textBody: { type: 'string', description: 'Plain text body (optional)', }, htmlBody: { type: 'string', description: 'HTML body (optional)', }, send: { type: ['boolean', 'string'], description: 'Whether to send the reply immediately (default: true). Set to false to save as draft instead.', }, replyTo: { type: 'array', items: { type: 'string' }, description: 'Reply-To email addresses (replies go here instead of to the sender)', }, }, required: ['originalEmailId'], }, }, { name: 'create_draft', description: 'Create an email draft without sending it. Supports threading headers for replies. IMPORTANT: each call creates a new draft — do not call twice for the same message.', inputSchema: { type: 'object', properties: { to: { type: 'array', items: { type: 'string' }, description: 'Recipient email addresses (optional)', }, cc: { type: 'array', items: { type: 'string' }, description: 'CC email addresses (optional)', }, bcc: { type: 'array', items: { type: 'string' }, description: 'BCC email addresses (optional)', }, from: { type: 'string', description: 'Sender email address (optional, defaults to account primary email)', }, mailboxId: { type: 'string', description: 'Mailbox ID to save the draft to (optional, defaults to Drafts folder)', }, subject: { type: 'string', description: 'Email subject (optional)', }, textBody: { type: 'string', description: 'Plain text body (optional)', }, htmlBody: { type: 'string', description: 'HTML body (optional)', }, inReplyTo: { type: 'array', items: { type: 'string' }, description: 'Message-IDs to reply to (optional, for threading)', }, references: { type: 'array', items: { type: 'string' }, description: 'Message-IDs for References header (optional, for threading)', }, replyTo: { type: 'array', items: { type: 'string' }, description: 'Reply-To email addresses (replies go here instead of to the sender)', }, }, }, }, { name: 'edit_draft', description: 'Edit an existing draft email. Since JMAP emails are immutable, this atomically destroys the old draft and creates a new one with the updated fields. Only fields you provide will be changed; others are preserved from the original draft.', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'The ID of the draft email to edit', }, to: { type: 'array', items: { type: 'string' }, description: 'Updated recipient email addresses (optional, keeps existing if omitted)', }, cc: { type: 'array', items: { type: 'string' }, description: 'Updated CC email addresses (optional)', }, bcc: { type: 'array', items: { type: 'string' }, description: 'Updated BCC email addresses (optional)', }, from: { type: 'string', description: 'Updated sender email address (optional)', }, subject: { type: 'string', description: 'Updated email subject (optional)', }, textBody: { type: 'string', description: 'Updated plain text body (optional)', }, htmlBody: { type: 'string', description: 'Updated HTML body (optional)', }, replyTo: { type: 'array', items: { type: 'string' }, description: 'Reply-To email addresses (replies go here instead of to the sender)', }, }, required: ['emailId'], }, }, { name: 'send_draft', description: 'Send an existing draft email. The draft must have recipients (to/cc/bcc) and a from address. After sending, the email is moved to the Sent folder and the draft keyword is removed.', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'The ID of the draft email to send', }, }, required: ['emailId'], }, }, { name: 'search_emails', description: 'Search emails by subject or content', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string', }, limit: { type: ['number', 'string'], description: 'Maximum number of results (default: 20)', default: 20, }, ascending: { type: 'boolean', description: 'Sort oldest first instead of newest first (default: false)', }, }, required: ['query'], }, }, { name: 'list_contacts', description: 'List contacts from the address book', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of contacts to return (default: 50)', default: 50, }, }, }, }, { name: 'get_contact', description: 'Get a specific contact by ID', inputSchema: { type: 'object', properties: { contactId: { type: 'string', description: 'ID of the contact to retrieve', }, }, required: ['contactId'], }, }, { name: 'search_contacts', description: 'Search contacts by name or email', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string', }, limit: { type: 'number', description: 'Maximum number of results (default: 20)', default: 20, }, }, required: ['query'], }, }, { name: 'list_calendars', description: 'List all calendars', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_calendar_events', description: 'List events from a calendar', inputSchema: { type: 'object', properties: { calendarId: { type: 'string', description: 'ID of the calendar (optional, defaults to all calendars)', }, startDate: { type: 'string', description: 'Filter events starting from this date (ISO 8601, e.g. 2026-03-23T00:00:00Z)', }, endDate: { type: 'string', description: 'Filter events ending before this date (ISO 8601, e.g. 2026-03-30T00:00:00Z)', }, limit: { type: 'number', description: 'Maximum number of events to return (default: 50)', default: 50, }, }, }, }, { name: 'get_calendar_event', description: 'Get a specific calendar event by ID', inputSchema: { type: 'object', properties: { eventId: { type: 'string', description: 'ID of the event to retrieve', }, }, required: ['eventId'], }, }, { name: 'create_calendar_event', description: 'Create a new calendar event', inputSchema: { type: 'object', properties: { calendarId: { type: 'string', description: 'ID of the calendar to create the event in', }, title: { type: 'string', description: 'Event title', }, description: { type: 'string', description: 'Event description (optional)', }, start: { type: 'string', description: 'Start time in ISO 8601 format', }, end: { type: 'string', description: 'End time in ISO 8601 format', }, location: { type: 'string', description: 'Event location (optional)', }, participants: { type: 'array', items: { type: 'object', properties: { email: { type: 'string' }, name: { type: 'string' } } }, description: 'Event participants (optional)', }, }, required: ['calendarId', 'title', 'start', 'end'], }, }, { name: 'list_identities', description: 'List sending identities (email addresses that can be used for sending)', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_recent_emails', description: 'Get the most recent emails from inbox (like top-ten)', inputSchema: { type: 'object', properties: { limit: { type: ['number', 'string'], description: 'Number of recent emails to retrieve (default: 10, max: 50)', default: 10, }, mailboxName: { type: 'string', description: 'Mailbox to search (default: inbox)', default: 'inbox', }, ascending: { type: 'boolean', description: 'Sort oldest first instead of newest first (default: false)', }, }, }, }, { name: 'mark_email_read', description: 'Mark an email as read or unread', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to mark', }, read: { type: 'boolean', description: 'true to mark as read, false to mark as unread', default: true, }, }, required: ['emailId'], }, }, { name: 'pin_email', description: 'Pin or unpin an email', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to pin/unpin', }, pinned: { type: 'boolean', description: 'true to pin, false to unpin', default: true, }, }, required: ['emailId'], }, }, { name: 'delete_email', description: 'Delete an email (move to trash)', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to delete', }, }, required: ['emailId'], }, }, { name: 'move_email', description: 'Move an email to a different mailbox', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to move', }, targetMailboxId: { type: 'string', description: 'ID of the target mailbox', }, }, required: ['emailId', 'targetMailboxId'], }, }, { name: 'add_labels', description: 'Add labels (mailboxes) to an email without removing existing ones', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to add labels to', }, mailboxIds: { type: 'array', items: { type: 'string' }, description: 'Array of mailbox IDs to add as labels', }, }, required: ['emailId', 'mailboxIds'], }, }, { name: 'remove_labels', description: 'Remove specific labels (mailboxes) from an email', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email to remove labels from', }, mailboxIds: { type: 'array', items: { type: 'string' }, description: 'Array of mailbox IDs to remove as labels', }, }, required: ['emailId', 'mailboxIds'], }, }, { name: 'get_email_attachments', description: 'Get list of attachments for an email', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email', }, }, required: ['emailId'], }, }, { name: 'download_attachment', description: 'Download an email attachment. If savePath is provided, saves the file to disk and returns the file path and size. Otherwise returns a download URL.', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'ID of the email', }, attachmentId: { type: 'string', description: 'ID of the attachment', }, savePath: { type: 'string', description: `File path to save the attachment to. Paths are restricted to ${getDownloadDir() || '~/Downloads/fastmail-mcp/'} (configurable via FASTMAIL_DOWNLOAD_DIR). Path traversal outside this directory is rejected for security. Parent directories will be created automatically.`, }, }, required: ['emailId', 'attachmentId'], }, }, { name: 'advanced_search', description: 'Advanced email search with multiple criteria', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Text to search for in subject/body', }, from: { type: 'string', description: 'Filter by sender email', }, to: { type: 'string', description: 'Filter by recipient email', }, subject: { type: 'string', description: 'Filter by subject', }, hasAttachment: { type: 'boolean', description: 'Filter emails with attachments', }, isUnread: { type: 'boolean', description: 'Filter unread emails', }, isPinned: { type: 'boolean', description: 'Filter pinned emails', }, mailboxId: { type: 'string', description: 'Search within specific mailbox', }, after: { type: 'string', description: 'Emails after this date (ISO 8601)', }, before: { type: 'string', description: 'Emails before this date (ISO 8601)', }, limit: { type: ['number', 'string'], description: 'Maximum results (default: 50)', default: 50, }, ascending: { type: 'boolean', description: 'Sort oldest first instead of newest first (default: false)', }, }, }, }, { name: 'get_thread', description: 'Get all emails in a conversation thread', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'ID of the thread/conversation', }, }, required: ['threadId'], }, }, { name: 'get_mailbox_stats', description: 'Get statistics for a mailbox (unread count, total emails, etc.)', inputSchema: { type: 'object', properties: { mailboxId: { type: 'string', description: 'ID of the mailbox (optional, defaults to all mailboxes)', }, }, }, }, { name: 'get_account_summary', description: 'Get overall account summary with statistics', inputSchema: { type: 'object', properties: {}, }, }, { name: 'bulk_mark_read', description: 'Mark multiple emails as read/unread', inputSchema: { type: 'object', properties: { emailIds: { type: 'array', items: { type: 'string' }, description: 'Array of email IDs to mark', }, read: { type: 'boolean', description: 'true to mark as read, false as unread', default: true, }, }, required: ['emailIds'], }, }, { name: 'bulk_pin', description: 'Pin or unpin multiple emails', inputSchema: { type: 'object', properties: { emailIds: { type: 'array', items: { type: 'string' }, description: 'Array of email IDs to pin/unpin', }, pinned: { type: 'boolean', description: 'true to pin, false to unpin', default: true, }, }, required: ['emailIds'], }, }, { name: 'bulk_move', description: 'Move multiple emails to a mailbox', inputSchema: { type: 'object', properties: { emailIds: { type: 'array', items: { type: 'string' }, description: 'Array of email IDs to move', }, targetMailboxId: { type: 'string', description: 'ID of target mailbox', }, }, required: ['emailIds', 'targetMailboxId'], }, }, { name: 'bulk_delete', description: 'Delete multiple emails (move to trash)', inputSchema: { type: 'object', properties: { emailIds: { type: 'array', items: { type: 'string' }, description: 'Array of email IDs to delete', }, }, required: ['emailIds'], }, }, { name: 'bulk_add_labels', description: 'Add labels to multiple emails simultaneously', inputSchema: { type: 'object', properties: { emailIds: { type: 'array', items: { type: 'string' }, description: 'Array of email IDs to add labels to', }, mailboxIds: { type: 'array', items: { type: 'string' }, description: 'Array of mailbox IDs to add as labels', }, }, required: ['emailIds', 'mailboxIds'], }, }, { name: 'bulk_remove_labels', description: 'Remove labels from multiple emails simultaneously', inputSchema: { type: 'object', properties: { emailIds: { type: 'array', items: { type: 'string' }, description: 'Array of email IDs to remove labels from', }, mailboxIds: { type: 'array', items: { type: 'string' }, description: 'Array of mailbox IDs to remove as labels', }, }, required: ['emailIds', 'mailboxIds'], }, }, { name: 'check_function_availability', description: 'Check which MCP functions are available based on account permissions', inputSchema: { type: 'object', properties: {}, }, }, { name: 'test_bulk_operations', description: 'Test bulk operations by finding recent emails and performing safe operations (mark read/unread)', inputSchema: { type: 'object', properties: { dryRun: { type: 'boolean', description: 'If true, only shows what would be done without making changes (default: true)', default: true, }, limit: { type: 'number', description: 'Number of emails to test with (default: 3, max: 10)', default: 3, }, }, }, }, ], }; });