reply_email
Compose a reply to an existing email, automatically adding In-Reply-To and References headers. Fetches the original message for context. Send now or save as draft.
Instructions
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.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| originalEmailId | Yes | ID of the email to reply to | |
| to | No | Recipient email addresses (optional, defaults to the original sender) | |
| cc | No | CC email addresses (optional) | |
| bcc | No | BCC email addresses (optional) | |
| from | No | Sender email address (optional, defaults to account primary email) | |
| textBody | No | Plain text body (optional) | |
| htmlBody | No | HTML body (optional) | |
| send | No | Whether to send the reply immediately (default: true). Set to false to save as draft instead. | |
| replyTo | No | Reply-To email addresses (replies go here instead of to the sender) |
Implementation Reference
- src/index.ts:240-289 (registration)Tool registration for 'reply_email' in ListToolsRequestSchema (inputSchema with description and properties like originalEmailId, to, cc, bcc, from, textBody, htmlBody, send, replyTo)
{ 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'], }, }, - src/index.ts:1076-1152 (handler)Handler for 'reply_email' in CallToolRequestSchema. Fetches original email to build threading headers (In-Reply-To, References), prepends 'Re:' to subject, defaults recipients to original sender, and either sends immediately or saves as draft based on the 'send' parameter.
case 'reply_email': { const { originalEmailId, to, cc, bcc, from, textBody, htmlBody, send, replyTo } = args as any; const shouldSend = coerceBool(send) ?? true; if (!originalEmailId) { throw new McpError(ErrorCode.InvalidParams, 'originalEmailId is required'); } if (shouldSend && !textBody && !htmlBody) { throw new McpError(ErrorCode.InvalidParams, 'Either textBody or htmlBody is required'); } // Fetch the original email to get threading headers const originalEmail = await client.getEmailById(originalEmailId); // Build threading headers const originalMessageId = originalEmail.messageId?.[0]; if (!originalMessageId) { throw new McpError(ErrorCode.InternalError, 'Original email does not have a Message-ID; cannot thread reply'); } const inReplyToHeader = [originalMessageId]; const referencesHeader = [ ...(originalEmail.references || []), originalMessageId, ]; // Build subject with Re: prefix let replySubject = originalEmail.subject || ''; if (!/^Re:/i.test(replySubject)) { replySubject = `Re: ${replySubject}`; } // Default recipients to the original sender const toArray = coerceStringArray(to); const replyRecipients = (toArray && toArray.length > 0) ? toArray : (Array.isArray(originalEmail.from) ? originalEmail.from.map((addr: any) => addr.email).filter(Boolean) : []); if (replyRecipients.length === 0) { throw new McpError(ErrorCode.InvalidParams, 'Could not determine reply recipient. Please provide "to" explicitly.'); } const replyParams = { to: replyRecipients, cc, bcc, from, subject: replySubject, textBody, htmlBody, inReplyTo: inReplyToHeader, references: referencesHeader, replyTo, }; if (!shouldSend) { const emailId = await client.createDraft(replyParams); return { content: [ { type: 'text', text: `Reply draft saved successfully (Email ID: ${emailId}). Subject: ${replySubject}`, }, ], }; } const submissionId = await client.sendEmail(replyParams); return { content: [ { type: 'text', text: `Reply sent successfully. Submission ID: ${submissionId}`, }, ], }; } - src/coerce.ts:33-38 (helper)Helper function coerceBool used in reply_email handler to coerce the 'send' parameter (boolean or string 'true'/'false') to a boolean value.
export function coerceBool(value: unknown): boolean | undefined { if (typeof value === 'boolean') return value; if (value === 'true') return true; if (value === 'false') return false; return undefined; } - src/coerce.ts:18-31 (helper)Helper function coerceStringArray used in reply_email handler to coerce the 'to' parameter (array or comma-separated string) to a string array for recipients.
export function coerceStringArray(value: unknown): string[] | undefined { if (value === undefined || value === null) return undefined; if (Array.isArray(value)) return value.map(String); if (typeof value !== 'string') return undefined; const trimmed = value.trim(); if (!trimmed) return []; if (trimmed.startsWith('[') && trimmed.endsWith(']')) { try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) return parsed.map(String); } catch { /* fall through to comma-split */ } } return trimmed.split(',').map(s => s.trim()).filter(Boolean); } - src/jmap-client.ts:237-377 (helper)sendEmail method on JmapClient - called by reply_email handler when shouldSend is true. Creates the email via Email/set and submits via EmailSubmission/set JMAP methods.
async sendEmail(email: { to: string[]; cc?: string[]; bcc?: string[]; subject: string; textBody?: string; htmlBody?: string; from?: string; mailboxId?: string; inReplyTo?: string[]; references?: string[]; replyTo?: string[]; }): Promise<string> { const session = await this.getSession(); // Get all identities to validate from address const identities = await this.getIdentities(); if (!identities || identities.length === 0) { throw new Error('No sending identities found'); } // Determine which identity to use let selectedIdentity; if (email.from) { // Validate that the from address matches an available identity selectedIdentity = identities.find(id => matchesIdentity(id.email, email.from!)); if (!selectedIdentity) { throw new Error('From address is not verified for sending. Choose one of your verified identities.'); } } else { // Use default identity selectedIdentity = identities.find(id => id.mayDelete === false) || identities[0]; } // Use the requested from address (not the identity email, which may be a wildcard like *@domain) const fromEmail = email.from || selectedIdentity.email; // Get the mailbox IDs we need const mailboxes = await this.getMailboxes(); const draftsMailbox = this.findMailboxByRoleOrName(mailboxes, 'drafts', 'draft'); const sentMailbox = this.findMailboxByRoleOrName(mailboxes, 'sent', 'sent'); if (!draftsMailbox) { throw new Error('Could not find Drafts mailbox to save email'); } if (!sentMailbox) { throw new Error('Could not find Sent mailbox to move email after sending'); } // Use provided mailboxId or default to drafts for initial creation const initialMailboxId = email.mailboxId || draftsMailbox.id; // Ensure we have at least one body type if (!email.textBody && !email.htmlBody) { throw new Error('Either textBody or htmlBody must be provided'); } const initialMailboxIds: Record<string, boolean> = {}; initialMailboxIds[initialMailboxId] = true; const sentMailboxIds: Record<string, boolean> = {}; sentMailboxIds[sentMailbox.id] = true; const emailObject = { mailboxIds: initialMailboxIds, keywords: { $draft: true }, from: [{ name: selectedIdentity.name, email: fromEmail }], to: email.to.map(addr => ({ email: addr })), cc: email.cc?.map(addr => ({ email: addr })) || [], bcc: email.bcc?.map(addr => ({ email: addr })) || [], subject: email.subject, ...(email.inReplyTo && { inReplyTo: email.inReplyTo }), ...(email.references && { references: email.references }), ...(email.replyTo?.length && { replyTo: email.replyTo.map(addr => ({ email: addr })) }), textBody: email.textBody ? [{ partId: 'text', type: 'text/plain' }] : undefined, htmlBody: email.htmlBody ? [{ partId: 'html', type: 'text/html' }] : undefined, bodyValues: { ...(email.textBody && { text: { value: email.textBody } }), ...(email.htmlBody && { html: { value: email.htmlBody } }) } }; const request: JmapRequest = { using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail', 'urn:ietf:params:jmap:submission'], methodCalls: [ ['Email/set', { accountId: session.accountId, create: { draft: emailObject } }, 'createEmail'], ['EmailSubmission/set', { accountId: session.accountId, create: { submission: { emailId: '#draft', identityId: selectedIdentity.id, envelope: { mailFrom: { email: fromEmail }, rcptTo: [ ...email.to.map(addr => ({ email: addr })), ...(email.cc || []).map(addr => ({ email: addr })), ...(email.bcc || []).map(addr => ({ email: addr })), ] } } }, onSuccessUpdateEmail: { '#submission': { mailboxIds: sentMailboxIds, keywords: { $seen: true } } } }, 'submitEmail'] ] }; const response = await this.makeRequest(request); const emailResult = this.getMethodResult(response, 0); if (emailResult.notCreated?.draft) { const err = emailResult.notCreated.draft; throw new Error(`Failed to create email: ${err.type}${err.description ? ' - ' + err.description : ''}`); } const emailId = emailResult.created?.draft?.id; if (!emailId) { throw new Error('Email creation returned no email ID'); } const submissionResult = this.getMethodResult(response, 1); if (submissionResult.notCreated?.submission) { const err = submissionResult.notCreated.submission; throw new Error(`Failed to submit email: ${err.type}${err.description ? ' - ' + err.description : ''}`); } const submissionId = submissionResult.created?.submission?.id; if (!submissionId) { throw new Error('Email submission returned no submission ID'); } return submissionId; }