feat(smtp): save sent messages to Sent folder via IMAP APPEND#47
Open
ilyasturki wants to merge 1 commit into
Open
feat(smtp): save sent messages to Sent folder via IMAP APPEND#47ilyasturki wants to merge 1 commit into
ilyasturki wants to merge 1 commit into
Conversation
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.
There was a problem hiding this comment.
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_sentconfig (schema/env/docs) and normalizes it toAccountConfig.saveToSent. - Refactors SMTP sending to pin
Message-ID/Date, then best-effort IMAP APPEND to Sent. - Adds IMAP
saveToSentimplementation 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 (includingsaveToSentbest-effort behavior and Message-ID/Date pinning) now applies tosendReply(and similarlysendForward/sendDraft), but the added tests only validatesendEmail. 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); | ||
| }); | ||
| }); | ||
| } |
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
After every successful SMTP send (
sendEmail,replyToEmail,forwardEmail,sendDraft), the same RFC822 message is APPENDed to the account's Sent folder with the\Seenflag. 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 amessageId, 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
save_to_sent(defaulttrue).save_to_sent = falseto disable per account.MCP_EMAIL_SAVE_TO_SENT=falsefor single-account env mode.\Sentspecial-use flag, with fallbacks to common names (Sent,[Gmail]/Sent Mail,INBOX.Sent,Sent Mail,Sent Items).SmtpServicebefore dispatch so the APPEND'd copy and the SMTP-delivered copy carry identical threading-critical headers.console.errorbut never fails the send (the recipient already got the message — we just couldn't archive locally).MailComposerusing the same options passed totransport.sendMail.Tests
smtp.service.test.tswith asave_to_sentsuite:saveToSentwhen enabledManual integration test
Ran end-to-end with two real accounts:
Sent)[Gmail]/Sent Mail)For both:
sendEmailsucceeds → 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.