edit_draft
Update a draft email by specifying only the fields you want to change. Atomically replaces the old draft with a new version, preserving all other fields.
Instructions
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.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| emailId | Yes | The ID of the draft email to edit | |
| to | No | Updated recipient email addresses (optional, keeps existing if omitted) | |
| cc | No | Updated CC email addresses (optional) | |
| bcc | No | Updated BCC email addresses (optional) | |
| from | No | Updated sender email address (optional) | |
| subject | No | Updated email subject (optional) | |
| textBody | No | Updated plain text body (optional) | |
| htmlBody | No | Updated HTML body (optional) | |
| replyTo | No | Reply-To email addresses (replies go here instead of to the sender) |
Implementation Reference
- src/index.ts:1192-1217 (handler)The handler for the 'edit_draft' tool in the CallToolRequestSchema switch case. It extracts args (emailId, to, cc, bcc, from, subject, textBody, htmlBody, replyTo), validates emailId is required, and calls client.updateDraft() which returns a new email ID.
case 'edit_draft': { const { emailId, to, cc, bcc, from, subject, textBody, htmlBody, replyTo } = args as any; if (!emailId) { throw new McpError(ErrorCode.InvalidParams, 'emailId is required'); } const newEmailId = await client.updateDraft(emailId, { to, cc, bcc, from, subject, textBody, htmlBody, replyTo, }); return { content: [ { type: 'text', text: `Draft updated successfully. New Email ID: ${newEmailId} (old draft ${emailId} was replaced)`, }, ], }; } - src/jmap-client.ts:484-613 (handler)The JmapClient.updateDraft() method implements the core logic: fetches existing draft, verifies it's a draft (has $draft keyword), resolves identity, merges updates with existing values, and atomically creates a new draft while destroying the old one via a single Email/set JMAP call.
async updateDraft(emailId: string, updates: { to?: string[]; cc?: string[]; bcc?: string[]; subject?: string; textBody?: string; htmlBody?: string; from?: string; replyTo?: string[]; }): Promise<string> { const session = await this.getSession(); // Fetch the existing email const getRequest: JmapRequest = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/get', { accountId: session.accountId, ids: [emailId], properties: ['id', 'subject', 'from', 'to', 'cc', 'bcc', 'replyTo', 'textBody', 'htmlBody', 'bodyValues', 'mailboxIds', 'keywords'], bodyProperties: ['partId', 'blobId', 'type', 'size'], fetchTextBodyValues: true, fetchHTMLBodyValues: true, }, 'getEmail'] ] }; const getResponse = await this.makeRequest(getRequest); const existingEmail = this.getListResult(getResponse, 0)[0]; if (!existingEmail) { throw new Error(`Email with ID '${emailId}' not found`); } // Verify it's a draft if (!existingEmail.keywords?.$draft) { throw new Error('Cannot edit a non-draft email'); } // Resolve identity const identities = await this.getIdentities(); if (!identities || identities.length === 0) { throw new Error('No sending identities found'); } let selectedIdentity; if (updates.from) { selectedIdentity = identities.find(id => matchesIdentity(id.email, updates.from!)); if (!selectedIdentity) { throw new Error('From address is not verified for sending. Choose one of your verified identities.'); } } else { // Use existing from, or fall back to default identity const existingFrom = existingEmail.from?.[0]?.email; if (existingFrom) { selectedIdentity = identities.find(id => matchesIdentity(id.email, existingFrom)) || identities.find(id => id.mayDelete === false) || identities[0]; } else { selectedIdentity = identities.find(id => id.mayDelete === false) || identities[0]; } } // Extract existing body values const existingTextBody = existingEmail.bodyValues ? Object.values(existingEmail.bodyValues).find((bv: any) => existingEmail.textBody?.some((tb: any) => tb.partId === (bv as any).partId || true) ) : null; const existingHtmlBody = existingEmail.bodyValues ? Object.values(existingEmail.bodyValues).find((bv: any) => existingEmail.htmlBody?.some((hb: any) => hb.partId === (bv as any).partId || true) ) : null; // Merge: updates override existing values const mergedSubject = updates.subject !== undefined ? updates.subject : (existingEmail.subject || ''); const mergedTo = updates.to !== undefined ? updates.to.map(addr => ({ email: addr })) : (existingEmail.to || []); const mergedCc = updates.cc !== undefined ? updates.cc.map(addr => ({ email: addr })) : (existingEmail.cc || []); const mergedBcc = updates.bcc !== undefined ? updates.bcc.map(addr => ({ email: addr })) : (existingEmail.bcc || []); const mergedReplyTo = updates.replyTo !== undefined ? updates.replyTo.map(addr => ({ email: addr })) : (existingEmail.replyTo || null); const textBodyValue = updates.textBody !== undefined ? updates.textBody : (existingTextBody as any)?.value; const htmlBodyValue = updates.htmlBody !== undefined ? updates.htmlBody : (existingHtmlBody as any)?.value; const emailObject: any = { mailboxIds: existingEmail.mailboxIds, keywords: { $draft: true }, from: [{ email: updates.from || existingEmail.from?.[0]?.email || selectedIdentity.email }], to: mergedTo, cc: mergedCc, bcc: mergedBcc, subject: mergedSubject, ...(mergedReplyTo?.length && { replyTo: mergedReplyTo }), }; if (textBodyValue) emailObject.textBody = [{ partId: 'text', type: 'text/plain' }]; if (htmlBodyValue) emailObject.htmlBody = [{ partId: 'html', type: 'text/html' }]; if (textBodyValue || htmlBodyValue) { emailObject.bodyValues = { ...(textBodyValue && { text: { value: textBodyValue } }), ...(htmlBodyValue && { html: { value: htmlBodyValue } }), }; } // Atomic create + destroy in a single Email/set call const request: JmapRequest = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'], methodCalls: [ ['Email/set', { accountId: session.accountId, create: { draft: emailObject }, destroy: [emailId], }, 'updateDraft'] ] }; const response = await this.makeRequest(request); const result = this.getMethodResult(response, 0); if (result.notCreated?.draft) { const err = result.notCreated.draft; throw new Error(`Failed to create updated draft: ${err.type}${err.description ? ' - ' + err.description : ''}`); } const newEmailId = result.created?.draft?.id; if (!newEmailId) { throw new Error('Draft update returned no email ID'); } return newEmailId; } - src/index.ts:349-398 (registration)Registration of the 'edit_draft' tool in the ListToolsRequestSchema handler, including its full inputSchema definition and description.
{ 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'], }, }, - src/index.ts:352-397 (schema)Input schema for the 'edit_draft' tool, defining required emailId and optional fields: to, cc, bcc, from, subject, textBody, htmlBody, replyTo.
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'], },