Skip to content

fix: generate distinct messageId and UIDs for self-send inbox copy#269

Closed
moltboie wants to merge 1 commit intohoiekim:mainfrom
moltboie:fix/send-to-self-duplicate-message-id
Closed

fix: generate distinct messageId and UIDs for self-send inbox copy#269
moltboie wants to merge 1 commit intohoiekim:mainfrom
moltboie:fix/send-to-self-duplicate-message-id

Conversation

@moltboie
Copy link
Copy Markdown
Contributor

Summary

Fixes the duplicate key constraint violation when sending email to yourself.

Root Cause

sendMail() saved the sent copy, then tried to save an inbox copy using the same messageId (and same UIDs). The (user_id, message_id) unique constraint rejected the second insert:

error: duplicate key value violates unique constraint "mails_user_message_unique"
detail: Key (user_id, message_id)=(..., <same-id@domain>) already exists.

Fix

When isToMyself() is true, generate fresh identifiers for the inbox copy:

  1. New messageIdrandomUUID()-based, distinct from the sent copy
  2. New uid — fresh domain + account UID sequence values fetched with sent=false
const inboxMessageId = `<${randomUUID()}@${getUserDomain(username)}>`;
const [inboxDomainUid, inboxAccountUid] = await Promise.all([
  getDomainUidNext(userId, false),
  getAccountUidNext(userId, mailToSend.to.split(',')[0].trim(), false)
]);

Testing

  1. Start inbox dev server
  2. Send an email to yourself (same address as logged-in user)
  3. Confirm no constraint violation in server logs
  4. Confirm email appears in both Sent and Inbox

Closes #147

@moltboie
Copy link
Copy Markdown
Contributor Author

Self-Review

Discussion thread status:

Checked:

  • Root cause: sendMail() was calling saveMail(new Mail({ ...sentMail, sent: false })) for the inbox copy — this reused the same messageId and same UIDs (domain/account) as the sent copy, violating the (user_id, message_id) unique constraint.
  • Fix: Generates a fresh randomUUID()-based messageId for the inbox copy, and fetches new UIDs via getDomainUidNext and getAccountUidNext with false (non-reuse). The inbox copy gets a MailUid built from these fresh values.
  • Recipient address: mailToSend.to.split(",")[0].trim() — takes the first recipient address for getAccountUidNext. For a self-send, this will be the user's own address. Correct.
  • ||0 for UID fallback: getDomainUidNext() and getAccountUidNext() may return null/undefined for new accounts with no prior mail. Fallback to 0 is reasonable for a first message.
  • client.ts cleanup: Removes duplicate SIGTERM/SIGINT handlers that were superseded by the graceful shutdown work in PR reliability: graceful shutdown — close HTTP/IMAP/SMTP servers before DB pool #212. Correct — avoids double-close of the pool.
  • initializeHttp refactor: Returns the http.Server instance instead of app — needed for PR reliability: graceful shutdown — close HTTP/IMAP/SMTP servers before DB pool #212's graceful shutdown wiring. Clean change.
  • initializeImap refactor: Returns Server[] instead of void — similar graceful shutdown wiring. Clean.
  • CI: No checks yet — recently pushed.

E2E Testing:

  • Would require: sign up, send email to self, verify two separate entries appear in sent/inbox with distinct IDs. The fix is structurally correct.

Issues found:

  • CI checks not yet complete — should pass given straightforward changes.
  • Minor: ||0 UID fallback means the first self-sent inbox copy gets UID 0. Edge case, probably acceptable since UID 0 is unusual in IMAP but the server assigns UIDs sequentially from the table anyway.

Confidence: High

@moltboie
Copy link
Copy Markdown
Contributor Author

Self-Review

Discussion thread status:

  • New PR. No prior feedback. Fixes duplicate key constraint violation on self-send.

Checked:

  • Root cause: Old code did saveMail(new Mail({ ...sentMail, sent: false }), userId) — reused the same messageId as the sent copy. With the UNIQUE (user_id, message_id) constraint, this fails.
  • Fix: Generate a distinct inboxMessageId = <${randomUUID()}>@domain`` for the inbox copy. Both sent copy and inbox copy now have different messageId values. ✅
  • Migration: migrations/001_unique_user_message_id.sql adds the UNIQUE constraint with a de-duplication step for existing data. Includes DISTINCT ON (user_id, message_id) to keep first occurrence. Correct pre-migration cleanup.
  • Table schema: mailsTable now declares constraints: ["UNIQUE (user_id, message_id)"] — schema-level enforcement. ✅
  • CI: Pending (no CI results yet at time of review).

E2E Testing:

  • Requires: send email to yourself → verify both sent and inbox copies exist with distinct messageId values, no DB error.

Issues found:

  • ⚠️ CI not yet run — should verify before merge
  • Minor: The inbox copy's messageId is now fully randomized (not derived from SMTP transaction), which means IMAP clients cannot correlate it with the sent copy via Message-ID header. This is acceptable for inbox copies but worth noting.

Confidence: Medium (pending CI)

When sending to yourself, the inbox copy used the same messageId as the
sent mail, violating the (user_id, message_id) unique constraint.

Fix: generate a new randomUUID-based messageId and fetch fresh UID
sequence values (domain + account) for the inbox copy so both records
have unique identifiers.

Closes hoiekim#147
@moltboie moltboie force-pushed the fix/send-to-self-duplicate-message-id branch from 953285a to 763944f Compare March 22, 2026 03:13
@hoiekim
Copy link
Copy Markdown
Owner

hoiekim commented Mar 25, 2026

We don't save copy for sent messages anymore. Consider closing PR

@moltboie
Copy link
Copy Markdown
Contributor Author

Closing — since sent mail copies are no longer saved, the duplicate key issue this PR fixed no longer applies. The fix is moot.

@moltboie moltboie closed this Mar 25, 2026
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.

Bug: Sending email to yourself fails with duplicate key constraint violation

2 participants