send_email
Send email messages with customizable recipients, subject, and body in plain text or HTML, including CC, BCC, and reply threading support.
Instructions
Send an email
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| to | Yes | Recipient email addresses (array of strings, or a comma-separated string) | |
| cc | No | CC email addresses (optional) | |
| bcc | No | BCC email addresses (optional) | |
| from | No | Sender email address (optional, defaults to account primary email) | |
| mailboxId | No | Mailbox ID to save the email to (optional, defaults to Drafts folder) | |
| subject | Yes | Email subject | |
| textBody | No | Plain text body (optional) | |
| htmlBody | No | HTML body (optional) | |
| inReplyTo | No | Message-ID(s) of the email being replied to (optional, for threading) | |
| references | No | Full reference chain of Message-IDs (optional, for threading) | |
| replyTo | No | Reply-To email addresses (replies go here instead of to the sender) |
Implementation Reference
- src/index.ts:178-239 (registration)Tool registration for 'send_email' in the ListToolsRequestSchema handler, including input schema (to, cc, bcc, from, mailboxId, subject, textBody, htmlBody, inReplyTo, references, replyTo).
{ 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'], }, }, - src/index.ts:1039-1074 (handler)Handler for 'send_email' in the CallToolRequestSchema switch statement. Validates arguments (to, subject, textBody/htmlBody), coerces 'to' to an array, then calls client.sendEmail().
case 'send_email': { const { to, cc, bcc, from, mailboxId, subject, textBody, htmlBody, inReplyTo, references, replyTo } = args as any; const toArray = coerceStringArray(to); if (!toArray || toArray.length === 0) { throw new McpError(ErrorCode.InvalidParams, 'to field is required and must be a non-empty array'); } if (!subject) { throw new McpError(ErrorCode.InvalidParams, 'subject is required'); } if (!textBody && !htmlBody) { throw new McpError(ErrorCode.InvalidParams, 'Either textBody or htmlBody is required'); } const submissionId = await client.sendEmail({ to: toArray, cc, bcc, from, mailboxId, subject, textBody, htmlBody, inReplyTo, references, replyTo, }); return { content: [ { type: 'text', text: `Email sent successfully. Submission ID: ${submissionId}`, }, ], }; } - src/jmap-client.ts:237-377 (handler)The core sendEmail() method on JmapClient that builds the JMAP Email/set and EmailSubmission/set requests to create and submit the email via the JMAP API.
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; } - src/coerce.ts:18-31 (helper)coerceStringArray helper used by the send_email handler to convert the 'to' parameter (which may be a string, comma-separated string, or array) into a string array.
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); }