1use core::{fmt, mem};
46
47use alloc::{
48 string::{String, ToString},
49 vec::Vec,
50};
51
52use imap_codec::{
53 CommandCodec,
54 fragmentizer::Fragmentizer,
55 imap_types::{
56 command::{Command, CommandBody},
57 core::{IString, NString, TagGenerator},
58 error::ValidationError,
59 response::{Capability, Code, Data, StatusKind, Tagged},
60 secret::Secret,
61 },
62};
63use log::{debug, trace};
64use thiserror::Error;
65
66use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
67
68#[derive(Clone, Debug, Error)]
70pub enum ImapLoginError {
71 #[error("IMAP LOGIN failed: NO {0}")]
73 No(String),
74 #[error("IMAP LOGIN failed: BAD {0}")]
76 Bad(String),
77 #[error("IMAP LOGIN failed: BYE {0}")]
79 Bye(String),
80 #[error("IMAP LOGIN failed: server did not return a tagged response")]
82 MissingTagged,
83 #[error("IMAP LOGIN failed: {0}")]
85 Send(#[from] ImapSendError),
86 #[error(transparent)]
88 Capability(#[from] ImapCapabilityGetError),
89 #[error(transparent)]
91 ServerId(#[from] ImapServerIdError),
92}
93
94#[derive(Clone, Debug, Default, Eq, PartialEq)]
96pub struct ImapLoginOptions {
97 pub ensure_capabilities: bool,
100 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
103}
104
105pub struct ImapLogin {
107 state: State,
108 observed: Vec<Capability<'static>>,
109 opts: ImapLoginOptions,
110}
111
112impl ImapLogin {
113 pub fn new(
119 user: impl AsRef<str>,
120 password: impl AsRef<str>,
121 opts: ImapLoginOptions,
122 ) -> Result<Self, ValidationError> {
123 let username = user.as_ref().to_string().try_into()?;
124 let password = Secret::new(password.as_ref().to_string().try_into()?);
125
126 let cmd = Command {
127 tag: TagGenerator::new().generate(),
128 body: CommandBody::Login { username, password },
129 };
130 trace!("send IMAP command {cmd:?}");
131 let send = ImapSend::new(CommandCodec::new(), cmd);
132
133 Ok(Self {
134 state: State::Send(send),
135 observed: Vec::new(),
136 opts,
137 })
138 }
139
140 fn wants_capability(&mut self) -> Option<State> {
141 (self.opts.ensure_capabilities && self.observed.is_empty())
142 .then(|| State::Capability(ImapCapabilityGet::new()))
143 }
144
145 fn wants_id(&mut self) -> Option<State> {
146 let params = self.opts.auto_id.take()?;
147 let wire = (!params.is_empty()).then_some(params);
148 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
149 parameters: wire,
150 })))
151 }
152}
153
154impl ImapCoroutine for ImapLogin {
155 type Yield = ImapYield;
156 type Return = Result<Vec<Capability<'static>>, ImapLoginError>;
157
158 fn resume(
159 &mut self,
160 fragmentizer: &mut Fragmentizer,
161 arg: Option<&[u8]>,
162 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
163 loop {
164 match &mut self.state {
165 State::Send(send) => {
166 let out = imap_try!(send, fragmentizer, arg);
167
168 if let Some(bye) = out.bye {
169 let err = ImapLoginError::Bye(bye.text.to_string());
170 return ImapCoroutineState::Complete(Err(err));
171 }
172
173 let Some(Tagged { body, .. }) = out.tagged else {
174 let err = ImapLoginError::MissingTagged;
175 return ImapCoroutineState::Complete(Err(err));
176 };
177
178 let code = match body.kind {
179 StatusKind::Ok => body.code,
180 StatusKind::No => {
181 let err = ImapLoginError::No(body.text.to_string());
182 return ImapCoroutineState::Complete(Err(err));
183 }
184 StatusKind::Bad => {
185 let err = ImapLoginError::Bad(body.text.to_string());
186 return ImapCoroutineState::Complete(Err(err));
187 }
188 };
189
190 let mut new_capability = None;
191
192 if let Some(Code::Capability(capability)) = code {
193 new_capability.replace(capability);
194 }
195
196 for data in out.data {
197 if let Data::Capability(capability) = data {
198 new_capability.replace(capability);
199 }
200 }
201
202 if let Some(capability) = new_capability {
203 self.observed = capability.into_iter().collect();
204 }
205
206 if let Some(next) = self.wants_capability() {
207 self.state = next;
208 debug!("{}", self.state);
209 continue;
210 }
211
212 if let Some(next) = self.wants_id() {
213 self.state = next;
214 debug!("{}", self.state);
215 continue;
216 }
217
218 let capability = mem::take(&mut self.observed);
219 return ImapCoroutineState::Complete(Ok(capability));
220 }
221 State::Capability(capability) => {
222 self.observed = imap_try!(capability, fragmentizer, arg);
223
224 if let Some(next) = self.wants_id() {
225 self.state = next;
226 debug!("{}", self.state);
227 continue;
228 }
229
230 let capability = mem::take(&mut self.observed);
231 return ImapCoroutineState::Complete(Ok(capability));
232 }
233 State::Id(id) => {
234 imap_try!(id, fragmentizer, arg);
235 let capability = mem::take(&mut self.observed);
236 return ImapCoroutineState::Complete(Ok(capability));
237 }
238 }
239 }
240 }
241}
242
243enum State {
244 Send(ImapSend<CommandCodec>),
245 Capability(ImapCapabilityGet),
246 Id(ImapServerId),
247}
248
249impl fmt::Display for State {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 Self::Send(_) => f.write_str("send login"),
253 Self::Capability(_) => f.write_str("fetch capabilities"),
254 Self::Id(_) => f.write_str("send id"),
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use core::str;
262
263 use alloc::format;
264
265 use crate::rfc3501::login::*;
266
267 #[test]
268 fn success_returns_ok() {
269 let opts = ImapLoginOptions::default();
270 let mut auth = ImapLogin::new("alice", "secret", opts).expect("valid credentials");
271 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
272
273 let bytes = expect_wants_write(&mut auth, &mut frag, None);
274 let line = str::from_utf8(&bytes).expect("utf8 command");
275 let tag = first_word(line);
276 assert!(line.contains("LOGIN "));
277
278 expect_wants_read(&mut auth, &mut frag);
279
280 let reply = format!("{tag} OK LOGIN completed\r\n");
281 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
282 }
283
284 #[test]
285 fn invalid_credentials_returns_no_error() {
286 let opts = ImapLoginOptions::default();
287 let mut auth = ImapLogin::new("alice", "wrong", opts).expect("valid credentials");
288 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
289
290 let bytes = expect_wants_write(&mut auth, &mut frag, None);
291 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
292
293 expect_wants_read(&mut auth, &mut frag);
294
295 let reply = format!("{tag} NO authentication failed\r\n");
296 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
297 let ImapLoginError::No(text) = err else {
298 panic!("expected ImapLoginError::No, got {err:?}");
299 };
300 assert_eq!(text, "authentication failed");
301 }
302
303 #[test]
304 fn tagged_bad_returns_bad_error() {
305 let opts = ImapLoginOptions::default();
306 let mut auth = ImapLogin::new("alice", "secret", opts).expect("valid credentials");
307 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
308
309 let bytes = expect_wants_write(&mut auth, &mut frag, None);
310 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
311
312 expect_wants_read(&mut auth, &mut frag);
313
314 let reply = format!("{tag} BAD LOGIN disabled\r\n");
315 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
316 let ImapLoginError::Bad(text) = err else {
317 panic!("expected ImapLoginError::Bad, got {err:?}");
318 };
319 assert_eq!(text, "LOGIN disabled");
320 }
321
322 #[test]
323 fn success_with_capability_code_observes_capability() {
324 let opts = ImapLoginOptions::default();
325 let mut auth = ImapLogin::new("alice", "secret", opts).expect("valid credentials");
326 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
327
328 let bytes = expect_wants_write(&mut auth, &mut frag, None);
329 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
330
331 expect_wants_read(&mut auth, &mut frag);
332
333 let reply = format!("{tag} OK [CAPABILITY IMAP4rev1 IDLE] LOGIN completed\r\n");
334 let caps = match auth.resume(&mut frag, Some(reply.as_bytes())) {
335 ImapCoroutineState::Complete(Ok(caps)) => caps,
336 state => panic!("expected Complete(Ok), got {state:?}"),
337 };
338 assert!(caps.iter().any(|c| matches!(c, Capability::Imap4Rev1)));
339 assert!(caps.iter().any(|c| matches!(c, Capability::Idle)));
340 }
341
342 #[test]
343 fn nul_in_password_fails_at_construction() {
344 let opts = ImapLoginOptions::default();
345 let result = ImapLogin::new("alice", "bad\0password", opts);
346 assert!(
347 result.is_err(),
348 "expected construction to refuse NUL in password",
349 );
350 }
351
352 fn expect_wants_write(
353 cor: &mut ImapLogin,
354 frag: &mut Fragmentizer,
355 arg: Option<&[u8]>,
356 ) -> Vec<u8> {
357 match cor.resume(frag, arg) {
358 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
359 state => panic!("expected WantsWrite, got {state:?}"),
360 }
361 }
362
363 fn expect_wants_read(cor: &mut ImapLogin, frag: &mut Fragmentizer) {
364 match cor.resume(frag, None) {
365 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
366 state => panic!("expected WantsRead, got {state:?}"),
367 }
368 }
369
370 fn expect_complete_ok(cor: &mut ImapLogin, frag: &mut Fragmentizer, reply: &[u8]) {
371 match cor.resume(frag, Some(reply)) {
372 ImapCoroutineState::Complete(Ok(_)) => {}
373 state => panic!("expected Complete(Ok), got {state:?}"),
374 }
375 }
376
377 fn expect_complete_err(
378 cor: &mut ImapLogin,
379 frag: &mut Fragmentizer,
380 reply: &[u8],
381 ) -> ImapLoginError {
382 match cor.resume(frag, Some(reply)) {
383 ImapCoroutineState::Complete(Err(err)) => err,
384 state => panic!("expected Complete(Err), got {state:?}"),
385 }
386 }
387
388 fn first_word(line: &str) -> &str {
389 line.split_whitespace()
390 .next()
391 .expect("first whitespace-separated token")
392 }
393}