Skip to main content

rusmes_imap/
handler.rs

1//! IMAP command handler dispatcher
2//!
3//! This module contains the top-level [`HandlerContext`] and the [`handle_command`]
4//! dispatcher. Actual implementations live in sub-modules:
5//!
6//! - [`crate::handler_auth`]    – LOGIN, LOGOUT
7//! - [`crate::handler_mailbox`] – SELECT/EXAMINE, LIST, LSUB, SUBSCRIBE/UNSUBSCRIBE,
8//!   CREATE, DELETE, RENAME, IDLE, NAMESPACE
9//! - [`crate::handler_message`] – FETCH, STORE, SEARCH, APPEND, COPY, MOVE, EXPUNGE,
10//!   CLOSE, UID sub-commands
11
12use crate::command::ImapCommand;
13use crate::handler_auth::{handle_login, handle_logout};
14use crate::handler_mailbox::{
15    handle_create, handle_create_special_use, handle_delete, handle_idle, handle_list, handle_lsub,
16    handle_namespace, handle_rename, handle_select, handle_subscribe, handle_unsubscribe,
17};
18use crate::handler_message::{
19    handle_append, handle_close, handle_copy, handle_expunge, handle_fetch, handle_move,
20    handle_search, handle_store, handle_uid,
21};
22use crate::mailbox_registry::MailboxRegistry;
23use crate::response::ImapResponse;
24use crate::session::ImapSession;
25use rusmes_auth::AuthBackend;
26use rusmes_storage::{MailboxStore, MessageStore, MetadataStore};
27use std::sync::Arc;
28
29/// Handler context for IMAP commands
30pub struct HandlerContext {
31    pub mailbox_store: Arc<dyn MailboxStore>,
32    pub message_store: Arc<dyn MessageStore>,
33    pub metadata_store: Arc<dyn MetadataStore>,
34    pub auth_backend: Arc<dyn AuthBackend>,
35    /// Cross-session mailbox notification registry (Cluster 10).
36    pub mailbox_registry: Arc<MailboxRegistry>,
37}
38
39impl HandlerContext {
40    /// Create a new handler context
41    pub fn new(
42        mailbox_store: Arc<dyn MailboxStore>,
43        message_store: Arc<dyn MessageStore>,
44        metadata_store: Arc<dyn MetadataStore>,
45        auth_backend: Arc<dyn AuthBackend>,
46    ) -> Self {
47        Self {
48            mailbox_store,
49            message_store,
50            metadata_store,
51            auth_backend,
52            mailbox_registry: Arc::new(MailboxRegistry::new()),
53        }
54    }
55
56    /// Create a handler context with a pre-existing registry (for sharing across server instances).
57    pub fn with_registry(
58        mailbox_store: Arc<dyn MailboxStore>,
59        message_store: Arc<dyn MessageStore>,
60        metadata_store: Arc<dyn MetadataStore>,
61        auth_backend: Arc<dyn AuthBackend>,
62        mailbox_registry: Arc<MailboxRegistry>,
63    ) -> Self {
64        Self {
65            mailbox_store,
66            message_store,
67            metadata_store,
68            auth_backend,
69            mailbox_registry,
70        }
71    }
72}
73
74/// Handle an IMAP command — dispatches to the appropriate sub-handler
75#[allow(clippy::too_many_arguments)]
76pub async fn handle_command(
77    ctx: &HandlerContext,
78    session: &mut ImapSession,
79    tag: &str,
80    command: ImapCommand,
81) -> anyhow::Result<ImapResponse> {
82    match command {
83        ImapCommand::Capability => handle_capability(tag).await,
84        ImapCommand::Noop => handle_noop(tag).await,
85        ImapCommand::Login { user, password } => {
86            handle_login(ctx, session, tag, &user, &password).await
87        }
88        ImapCommand::Authenticate {
89            mechanism,
90            initial_response,
91        } => {
92            // Note: This is a placeholder. AUTHENTICATE requires special handling
93            // in the server loop because it's a multi-step process.
94            // The actual implementation is in crate::authenticate module.
95            let _ = (mechanism, initial_response);
96            Ok(ImapResponse::bad(
97                tag,
98                "AUTHENTICATE must be handled by server loop",
99            ))
100        }
101        ImapCommand::Logout => handle_logout(session, tag).await,
102        ImapCommand::Select { mailbox } => handle_select(ctx, session, tag, &mailbox, false).await,
103        ImapCommand::Examine { mailbox } => handle_select(ctx, session, tag, &mailbox, true).await,
104        ImapCommand::Fetch { sequence, items } => {
105            handle_fetch(ctx, session, tag, &sequence, &items).await
106        }
107        ImapCommand::Store {
108            sequence,
109            mode,
110            flags,
111        } => handle_store(ctx, session, tag, &sequence, mode, &flags).await,
112        ImapCommand::Search { criteria } => handle_search(ctx, session, tag, &criteria).await,
113        ImapCommand::List { reference, mailbox } => {
114            handle_list(ctx, session, tag, &reference, &mailbox).await
115        }
116        ImapCommand::Lsub { reference, mailbox } => {
117            handle_lsub(ctx, session, tag, &reference, &mailbox).await
118        }
119        ImapCommand::Subscribe { mailbox } => handle_subscribe(ctx, session, tag, &mailbox).await,
120        ImapCommand::Unsubscribe { mailbox } => {
121            handle_unsubscribe(ctx, session, tag, &mailbox).await
122        }
123        ImapCommand::Create { mailbox } => handle_create(ctx, session, tag, &mailbox).await,
124        ImapCommand::CreateSpecialUse {
125            mailbox,
126            special_use,
127        } => handle_create_special_use(ctx, session, tag, &mailbox, &special_use).await,
128        ImapCommand::Delete { mailbox } => handle_delete(ctx, session, tag, &mailbox).await,
129        ImapCommand::Rename { old, new } => handle_rename(ctx, session, tag, &old, &new).await,
130        ImapCommand::Append {
131            mailbox,
132            flags,
133            date_time,
134            message_literal,
135        } => {
136            handle_append(
137                ctx,
138                session,
139                tag,
140                &mailbox,
141                &flags,
142                date_time.as_deref(),
143                &message_literal,
144            )
145            .await
146        }
147        ImapCommand::Copy { sequence, mailbox } => {
148            handle_copy(ctx, session, tag, &sequence, &mailbox).await
149        }
150        ImapCommand::Move { sequence, mailbox } => {
151            handle_move(ctx, session, tag, &sequence, &mailbox).await
152        }
153        ImapCommand::Expunge => handle_expunge(ctx, session, tag).await,
154        ImapCommand::Close => handle_close(ctx, session, tag).await,
155        ImapCommand::Idle => handle_idle(ctx, session, tag).await,
156        ImapCommand::Namespace => handle_namespace(tag, session).await,
157        ImapCommand::Uid { subcommand } => handle_uid(ctx, session, tag, subcommand.as_ref()).await,
158        ImapCommand::Compress { mechanism } => handle_compress(session, tag, &mechanism).await,
159    }
160}
161
162/// Handle CAPABILITY command
163async fn handle_capability(tag: &str) -> anyhow::Result<ImapResponse> {
164    // Return basic IMAP4rev1 capabilities
165    let capabilities = vec![
166        "IMAP4rev1",
167        "LITERAL+",
168        "SASL-IR",
169        "LOGIN-REFERRALS",
170        "ID",
171        "ENABLE",
172        "IDLE",
173        "NAMESPACE",
174        "UIDPLUS",
175        "LIST-EXTENDED",
176        "UNSELECT",
177        "CHILDREN",
178        "SPECIAL-USE",
179        "MOVE",
180        // Compression (RFC 4978)
181        "COMPRESS=DEFLATE",
182        // SASL authentication mechanisms (RFC 3501 Section 6.2.2)
183        "AUTH=PLAIN",
184        "AUTH=LOGIN",
185        "AUTH=CRAM-MD5",
186        "AUTH=SCRAM-SHA-256",
187        "AUTH=XOAUTH2",
188    ];
189
190    let cap_list = capabilities.join(" ");
191    Ok(ImapResponse::ok(
192        tag,
193        format!("[CAPABILITY {}] Capability completed", cap_list),
194    ))
195}
196
197/// Handle NOOP command
198async fn handle_noop(tag: &str) -> anyhow::Result<ImapResponse> {
199    Ok(ImapResponse::ok(tag, "NOOP completed"))
200}
201
202/// Handle COMPRESS command (RFC 4978).
203///
204/// This handler only validates the mechanism and sets the `compress_pending` flag on the session.
205/// The actual stream wrapping is done by `server::imap_session_loop` immediately after this
206/// response is sent and flushed, using `oxiarc_deflate::raw_stream::{RawInflateReader,
207/// RawDeflateWriter}`.
208async fn handle_compress(
209    session: &mut ImapSession,
210    tag: &str,
211    mechanism: &str,
212) -> anyhow::Result<ImapResponse> {
213    if !mechanism.eq_ignore_ascii_case("DEFLATE") {
214        return Ok(ImapResponse::no(
215            tag,
216            format!("COMPRESS: unsupported mechanism {mechanism}"),
217        ));
218    }
219    // Only allow compression once per session (RFC 4978 §3).
220    if session.compress_pending {
221        return Ok(ImapResponse::no(tag, "COMPRESS: already active"));
222    }
223    // Signal the server loop to swap the stream after the OK is written.
224    session.compress_pending = true;
225    Ok(ImapResponse::ok(
226        tag,
227        "[COMPRESSIONACTIVE] Begin DEFLATE compression",
228    ))
229}