Skip to content

feat(smtp): save sent messages to Sent folder via IMAP APPEND#47

Open
ilyasturki wants to merge 1 commit into
codefuturist:mainfrom
ilyasturki:feat/save-to-sent
Open

feat(smtp): save sent messages to Sent folder via IMAP APPEND#47
ilyasturki wants to merge 1 commit into
codefuturist:mainfrom
ilyasturki:feat/save-to-sent

Conversation

@ilyasturki

Copy link
Copy Markdown

What

After every successful SMTP send (sendEmail, replyToEmail, forwardEmail, sendDraft), the same RFC822 message is APPENDed to the account's Sent folder with the \Seen flag. Without this, messages sent through the MCP never appear in webmail or desktop clients (Thunderbird, Apple Mail), and threads look broken — the recipient sees the reply, but the sender does not.

Why

This is the standard behavior of every desktop mail client. Currently after a reply_email, the recipient gets the message and the MCP returns a messageId, but the user's Sent folder stays empty, so Thunderbird/webmail show the conversation with a gap where the user's reply should be.

Closes #20.

Changes

  • New per-account config flag save_to_sent (default true).
    • TOML: save_to_sent = false to disable per account.
    • Env: MCP_EMAIL_SAVE_TO_SENT=false for single-account env mode.
  • Sent folder auto-detected via the IMAP \Sent special-use flag, with fallbacks to common names (Sent, [Gmail]/Sent Mail, INBOX.Sent, Sent Mail, Sent Items).
  • Message-ID and Date are now pinned in SmtpService before dispatch so the APPEND'd copy and the SMTP-delivered copy carry identical threading-critical headers.
  • Best-effort: a failed APPEND is logged via console.error but never fails the send (the recipient already got the message — we just couldn't archive locally).
  • Raw bytes for the APPEND are built once via nodemailer's MailComposer using the same options passed to transport.sendMail.

Tests

  • Extend smtp.service.test.ts with a save_to_sent suite:
    • calls saveToSent when enabled
    • skips when disabled (default in mocks)
    • swallows IMAP errors so the send still succeeds
  • Cover Message-ID/Date pinning in the send payload.
  • All 154 tests pass (was 150).

Manual integration test

Ran end-to-end with two real accounts:

  • Purelymail (auto-detected Sent at Sent)
  • Gmail (auto-detected Sent at [Gmail]/Sent Mail)

For both: sendEmail succeeds → APPEND appears in Sent folder, viewable in Thunderbird and provider webmail. Threading in Thunderbird's "Open in Conversation" view (Ctrl+Shift+O) now shows the full back-and-forth.

After every successful SMTP send (sendEmail, replyToEmail, forwardEmail,
sendDraft), the same RFC822 message is APPENDed to the account's Sent
folder with the \Seen flag. Without this, messages sent through the MCP
never appear in webmail or desktop clients (Thunderbird, Apple Mail) and
threads look broken (the recipient sees the reply but the sender does
not).

- New per-account config flag `save_to_sent` (default `true`).
  Override via TOML `save_to_sent = false` or env
  `MCP_EMAIL_SAVE_TO_SENT=false`.
- Sent folder auto-detected via the IMAP `\Sent` special-use flag, with
  fallbacks to common names (Sent, [Gmail]/Sent Mail, INBOX.Sent, ...).
- Message-ID and Date are now pinned in SmtpService before dispatch so
  the APPEND'd copy and the SMTP-delivered copy carry identical
  threading-critical headers.
- Best-effort: a failed APPEND is logged via console.error but never
  fails the send (the recipient already got the message).
- Raw bytes for the APPEND are built once via nodemailer's MailComposer
  using the same options passed to `transport.sendMail`.

Tests:
- Extend smtp.service.test.ts with save_to_sent suite (calls saveToSent
  when enabled, skips when disabled, swallows IMAP errors).
- Cover Message-ID/Date pinning in send payload.

Closes codefuturist#20.
Copilot AI review requested due to automatic review settings May 25, 2026 18:51
@ilyasturki ilyasturki requested a review from codefuturist as a code owner May 25, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a per-account save_to_sent feature that, after a successful SMTP send, optionally APPENDs the same message into the account’s IMAP Sent folder so it appears in mail clients.

Changes:

  • Introduces save_to_sent config (schema/env/docs) and normalizes it to AccountConfig.saveToSent.
  • Refactors SMTP sending to pin Message-ID/Date, then best-effort IMAP APPEND to Sent.
  • Adds IMAP saveToSent implementation and corresponding unit tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/types/index.ts Adds AccountConfig.saveToSent flag to drive the new behavior.
src/config/schema.ts Adds save_to_sent with default true to the validated config schema.
src/config/loader.ts Loads/normalizes save_to_sent from env/TOML and updates sample config comments.
src/services/smtp.service.ts Adds dispatch() to pin headers, send via SMTP, and optionally APPEND to Sent.
src/services/imap.service.ts Implements Sent-folder autodetection and APPEND logic.
src/services/smtp.service.test.ts Adds tests for Message-ID/Date pinning and save_to_sent behavior.
src/services/watcher.service.test.ts Updates test account to include the new required config field.
src/cli/account-commands.ts Ensures generated account configs include save_to_sent / saveToSent.
README.md Documents the new save_to_sent config option.
Comments suppressed due to low confidence (1)

src/services/smtp.service.ts:1

  • The new dispatch() path (including saveToSent best-effort behavior and Message-ID/Date pinning) now applies to sendReply (and similarly sendForward / sendDraft), but the added tests only validate sendEmail. Add coverage for at least one of these other send methods to ensure the Sent APPEND behavior and header pinning work consistently across all entry points.
/**

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +80 to +83
return {
messageId: typeof info.messageId === 'string' && info.messageId ? info.messageId : messageId,
status: 'sent',
};
Comment on lines +28 to +32
function generateMessageId(fromEmail: string): string {
const domain = fromEmail.split('@')[1] ?? 'localhost';
const rnd = Math.random().toString(36).slice(2) + Date.now().toString(36);
return `<${rnd}@${domain}>`;
}
* No MCP dependency — fully unit-testable.
*/

import MailComposer from 'nodemailer/lib/mail-composer/index.js';
Comment on lines +13 to +25
type MailOptions = Record<string, unknown>;

/** Build the raw RFC822 bytes for a message via nodemailer's MailComposer. */
async function buildRaw(mailOptions: MailOptions): Promise<Buffer> {
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const composer = new (MailComposer as any)(mailOptions);
composer.compile().build((err: Error | null, raw: Buffer) => {
if (err) reject(err);
else resolve(raw);
});
});
}
Comment thread src/config/loader.ts
max_messages: parseInt(process.env.MCP_EMAIL_SMTP_POOL_MAX_MESSAGES ?? '100', 10),
},
},
save_to_sent: process.env.MCP_EMAIL_SAVE_TO_SENT !== 'false',
Comment on lines +165 to +167
expect(Buffer.isBuffer(rawArg)).toBe(true);
expect((rawArg as Buffer).toString('utf-8')).toContain('Subject: S');
expect(dateArg).toBeInstanceOf(Date);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Save sent emails to IMAP Sent folder (APPEND after SMTP send)

2 participants