diff --git a/CHANGELOG.md b/CHANGELOG.md index bbdd2c53..9924a20c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - Client secret verification for confidential clients. - HTTP: Add `redirectRoot` option to `Http` object to allow redirecting requests to the root path to a different path (e.g. `/account`). - ACME: `reuseKey` option to allow reusing private keys in renewals. +- IMAP: `GETJMAPACCESS` command to discover the JMAP session resource URL (#2736). ## Changed diff --git a/crates/imap-proto/src/lib.rs b/crates/imap-proto/src/lib.rs index e4140ffe..6647cc63 100644 --- a/crates/imap-proto/src/lib.rs +++ b/crates/imap-proto/src/lib.rs @@ -74,6 +74,9 @@ pub enum Command { // RFC 9208 GetQuota, GetQuotaRoot, + + // RFC 9698 + GetJmapAccess, } impl Command { diff --git a/crates/imap-proto/src/parser/mod.rs b/crates/imap-proto/src/parser/mod.rs index f93c28d9..95688fca 100644 --- a/crates/imap-proto/src/parser/mod.rs +++ b/crates/imap-proto/src/parser/mod.rs @@ -80,6 +80,7 @@ impl CommandParser for Command { "ID" => Command::Id, "GETQUOTA" => Command::GetQuota, "GETQUOTAROOT" => Command::GetQuotaRoot, + "GETJMAPACCESS" => Command::GetJmapAccess, ) } @@ -413,7 +414,16 @@ impl PushUnique for Vec { #[cfg(test)] mod tests { - use crate::protocol::Sequence; + use crate::{Command, protocol::Sequence, receiver::CommandParser}; + + #[test] + fn parse_command() { + assert_eq!( + Command::parse(b"GETJMAPACCESS", false), + Some(Command::GetJmapAccess) + ); + assert_eq!(Command::parse(b"NOTACOMMAND", false), None); + } #[test] fn parse_sequence_set() { diff --git a/crates/imap-proto/src/protocol/capability.rs b/crates/imap-proto/src/protocol/capability.rs index 177377a4..9a97ab79 100644 --- a/crates/imap-proto/src/protocol/capability.rs +++ b/crates/imap-proto/src/protocol/capability.rs @@ -142,11 +142,11 @@ impl Capability { Capability::LiteralPlus, Capability::Id, Capability::Utf8Accept, - Capability::JmapAccess, ]; if is_authenticated { capabilities.extend([ + Capability::JmapAccess, Capability::Idle, Capability::Namespace, Capability::Children, diff --git a/crates/imap-proto/src/protocol/mod.rs b/crates/imap-proto/src/protocol/mod.rs index 56b9830c..64117f48 100644 --- a/crates/imap-proto/src/protocol/mod.rs +++ b/crates/imap-proto/src/protocol/mod.rs @@ -726,6 +726,7 @@ impl Display for Command { Command::Id => write!(f, "ID"), Command::GetQuota => write!(f, "GETQUOTA"), Command::GetQuotaRoot => write!(f, "GETQUOTAROOT"), + Command::GetJmapAccess => write!(f, "GETJMAPACCESS"), } } } diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index 62fabd1b..a3a703b9 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -245,6 +245,10 @@ impl Session { .handle_id(request) .await .map(|_| SessionResult::Continue), + Command::GetJmapAccess => self + .handle_jmap_access(request) + .await + .map(|_| SessionResult::Continue), }; match result { @@ -375,7 +379,8 @@ impl Session { | Command::MyRights | Command::Unauthenticate | Command::GetQuota - | Command::GetQuotaRoot => { + | Command::GetQuotaRoot + | Command::GetJmapAccess => { if let State::Authenticated { .. } | State::Selected { .. } = state { Ok(request) } else { diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index 82f8a4f4..2952747c 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -11,7 +11,7 @@ use common::network::SessionStream; use imap_proto::{ Command, StatusResponse, protocol::{ - ImapResponse, + ImapResponse, quoted_string, capability::{Capability, Response}, }, receiver::Request, @@ -73,4 +73,33 @@ impl Session { ) .await } + + pub async fn handle_jmap_access(&mut self, request: Request) -> trc::Result<()> { + // Validate access + self.assert_has_permission(Permission::ImapCapability)?; + + let op_start = Instant::now(); + trc::event!( + Imap(trc::ImapEvent::Capabilities), + SpanId = self.session_id, + Elapsed = op_start.elapsed() + ); + + let mut response = b"* JMAPACCESS ".to_vec(); + quoted_string( + &mut response, + &format!( + "{}/.well-known/jmap", + self.server.core.network.http.url_https + ), + ); + response.extend_from_slice(b"\r\n"); + + self.write_bytes( + StatusResponse::completed(Command::GetJmapAccess) + .with_tag(request.tag) + .serialize(response), + ) + .await + } } diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 404c7607..37212f8e 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -232,6 +232,13 @@ pub async fn imap_tests() { imap.authenticate(account.name(), account.secret()).await; } + // Test GETJMAPACCESS (RFC 9698) + imap.send("GETJMAPACCESS").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* JMAPACCESS \"") + .assert_contains("/.well-known/jmap\""); + // Delete folders for mailbox in ["Drafts", "Junk Mail", "Sent Items"] { imap.send(&format!("DELETE \"{}\"", mailbox)).await;