Skip to content

imap: wire COPYUID capture once async-imap exposes ResponseCode::CopyUid #107

@randomparity

Description

@randomparity

Context

PR #106 landed sub-group 2 of the open-issues roadmap (#70, #96, #97). The #96 work is partial: `Connection::move_messages` guards UIDVALIDITY correctly but cannot populate `MoveResult.new_uid` because `async-imap` does not expose the `[COPYUID uidvalidity src-uids dst-uids]` UIDPLUS response code.

What exists today (on `main` after #106 merges)

  • `rimap_imap::types::MoveResult { old_uid: Uid, new_uid: Option, used_fallback_reason: Option }`.
  • Every `MoveResult` returned by `move_messages` currently has `new_uid: None` and `used_fallback_reason: Some("async_imap_copyuid_unavailable".to_string())` — a stable marker the audit layer / agent can filter on.
  • `MoveOutcome { results, used_fallback, source_uid_validity, destination_uid_validity }` already surfaces both folder UIDVALIDITY values for audit correlation, so the loss of per-result `new_uid` is bounded.
  • `ResponseCode::CopyUid(u32 /* uidvalidity /, Vec / src /, Vec / dst */)` exists in `imap-proto 0.16.6` (`types.rs:139`) but `async-imap 0.11.2`'s session methods (`uid_copy`, `uid_mv`) return `Result<()>` and discard response codes.

What this issue tracks

Once `async-imap` exposes the UIDPLUS response code — either via a new session method signature (e.g. `Result`) or via raw-command machinery that cleanly surfaces `Response::Done { code: Option, ... }` — flip `build_results` in `crates/rimap-imap/src/ops/move_message.rs` to:

  1. Parse the `[COPYUID ...]` response code when available.
  2. Populate `MoveResult.new_uid: Some(Uid::new(dst_uid))` for each matched source UID.
  3. Set `used_fallback_reason: None` on results with populated `new_uid`.
  4. Keep the current fallback (`new_uid: None` + `used_fallback_reason: Some("server_no_uidplus")` or similar) for servers that do not advertise UIDPLUS.

Acceptance criteria

  1. When the server advertises UIDPLUS (`has_uidplus`) AND async-imap surfaces the response code, `MoveResult.new_uid` is populated with the mapped destination UID for every moved message. Verified via a Dovecot integration test that checks `new_uid` is `Some(...)` on a simple 3-message MOVE.

  2. When the server does NOT advertise UIDPLUS, `new_uid` stays `None` with `used_fallback_reason: Some("server_no_uidplus".to_string())` (or equivalent).

  3. `Connection::move_messages` signature is unchanged — flip is a behavior change, not an API change.

  4. `MoveMessageMeta` (in `rimap-server/src/tools/mailbox/move_message.rs`) surfaces the captured `new_uid` values in the per-result entries automatically; no server-side code change needed beyond what feat: UIDVALIDITY correctness — Option<u32>, MOVE guard, response echo, input check #106 already landed.

Blockers / watch

  • Upstream: track `async-imap` releases for UIDPLUS response-code support.
  • Fallback: if async-imap doesn't land the feature in a reasonable timeframe, consider using async-imap's internal `run_command_and_read_response` (or equivalent) to issue `UID COPY` / `UID MOVE` as raw commands and parse the tagged response manually. That's more invasive but removes the external dependency.

References

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions