@@ -35,6 +35,13 @@ use std::{
3535} ;
3636
3737use anyhow:: Result ;
38+ #[ cfg( feature = "imap" ) ]
39+ use anyhow:: anyhow;
40+ #[ cfg( feature = "imap" ) ]
41+ use io_imap:: types:: {
42+ IntoStatic ,
43+ core:: { IString , NString } ,
44+ } ;
3845use pimalaya_config:: command;
3946#[ cfg( feature = "imap" ) ]
4047use pimalaya_config:: secret:: Secret ;
@@ -145,6 +152,85 @@ pub struct ImapConfig {
145152 /// Optional SASL credentials. When omitted, the connection skips
146153 /// authentication entirely (no `AUTHENTICATE` is sent).
147154 pub sasl : Option < SaslConfig > ,
155+
156+ /// RFC 2971 `ID` extension quirks. Some providers (notably
157+ /// mail.qq.com, fastmail) require an `ID` exchange straight after
158+ /// authentication; set `id.auto = true` to opt in.
159+ #[ serde( default ) ]
160+ pub id : ImapIdConfig ,
161+ }
162+
163+ /// Per-account `imap.id.*` quirks.
164+ #[ derive( Clone , Debug , Default , Deserialize , Serialize ) ]
165+ #[ serde( rename_all = "kebab-case" , deny_unknown_fields) ]
166+ pub struct ImapIdConfig {
167+ /// When `true`, the auth coroutine chains an `ID` round-trip
168+ /// after the tagged auth response. Default `false` skips ID
169+ /// entirely.
170+ #[ serde( default ) ]
171+ pub auto : bool ,
172+
173+ /// Parameters sent with the auto-ID command. Empty (default)
174+ /// sends `ID NIL`. For each entry: `true` substitutes mirador's
175+ /// canned value for the well-known keys (`name`, `version`,
176+ /// `vendor`, `support-url`) or `NIL` for unknown keys; `false`
177+ /// always sends `NIL`. Keys absent from this map are not
178+ /// transmitted.
179+ #[ serde( default ) ]
180+ pub fields : HashMap < String , bool > ,
181+ }
182+
183+ /// Resolves an [`ImapIdConfig`] into the wire-level parameter list
184+ /// passed to the io-imap auth coroutines.
185+ ///
186+ /// [`None`] when `auto = false`; otherwise a vec where each entry
187+ /// maps the user-supplied key to either mirador's canned value
188+ /// (when the user set `true` and the key is well-known) or `NIL`.
189+ /// Unknown keys with `true` log a warning and fall back to `NIL`.
190+ #[ cfg( feature = "imap" ) ]
191+ pub fn resolve_auto_id_params (
192+ config : & ImapIdConfig ,
193+ ) -> Result < Option < Vec < ( IString < ' static > , NString < ' static > ) > > > {
194+ if !config. auto {
195+ return Ok ( None ) ;
196+ }
197+
198+ let mut params = Vec :: with_capacity ( config. fields . len ( ) ) ;
199+ for ( key, & use_canned) in & config. fields {
200+ let ikey = IString :: try_from ( key. clone ( ) )
201+ . map_err ( |err| anyhow ! ( "Invalid IMAP ID parameter key `{key}`: {err}" ) ) ?
202+ . into_static ( ) ;
203+
204+ let nval = if use_canned {
205+ match canned_imap_id_value ( key) {
206+ Some ( value) => NString :: try_from ( value)
207+ . map_err ( |err| {
208+ anyhow ! ( "Invalid canned IMAP ID value `{value}` for `{key}`: {err}" )
209+ } ) ?
210+ . into_static ( ) ,
211+ None => {
212+ log:: warn!( "imap.id.fields.{key} = true: no canned value defined, sending NIL" ) ;
213+ NString :: NIL
214+ }
215+ }
216+ } else {
217+ NString :: NIL
218+ } ;
219+
220+ params. push ( ( ikey, nval) ) ;
221+ }
222+ Ok ( Some ( params) )
223+ }
224+
225+ #[ cfg( feature = "imap" ) ]
226+ fn canned_imap_id_value ( key : & str ) -> Option < & ' static str > {
227+ match key {
228+ "name" => Some ( env ! ( "CARGO_PKG_NAME" ) ) ,
229+ "version" => Some ( env ! ( "CARGO_PKG_VERSION" ) ) ,
230+ "vendor" => Some ( "Pimalaya" ) ,
231+ "support-url" => Some ( "https://github.com/pimalaya/mirador" ) ,
232+ _ => None ,
233+ }
148234}
149235
150236// ---- JMAP ---------------------------------------------------------
0 commit comments