imap_client/
credentials.rs1use std::fmt;
2use zeroize::{Zeroize, ZeroizeOnDrop};
3
4use crate::error::ClientError;
5
6#[derive(Clone, Zeroize, ZeroizeOnDrop)]
8pub struct Password(String);
9
10impl Password {
11 pub fn new<S: Into<String>>(pass: S) -> Self {
12 Self(pass.into())
13 }
14
15 pub fn as_str(&self) -> &str {
16 &self.0
17 }
18
19 pub fn as_imap_quoted(&self) -> Result<String, ClientError> {
28 imap_quoted(&self.0)
29 }
30}
31
32impl fmt::Debug for Password {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str("\"***\"")
35 }
36}
37
38#[derive(Clone, Zeroize, ZeroizeOnDrop)]
40pub struct OAuthToken(String);
41
42impl OAuthToken {
43 pub fn new<S: Into<String>>(token: S) -> Self {
44 Self(token.into())
45 }
46
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl fmt::Debug for OAuthToken {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str("\"***\"")
55 }
56}
57
58pub(crate) fn imap_quoted(s: &str) -> Result<String, ClientError> {
61 if s.bytes()
62 .any(|b| b == b'\r' || b == b'\n' || b == 0 || b > 0x7F)
63 {
64 return Err(ClientError::CommandFailed(
65 "value contains characters disallowed in IMAP quoted string; \
66 use AUTHENTICATE for 8-bit or control-byte values"
67 .to_string(),
68 ));
69 }
70 let mut out = String::with_capacity(s.len() + 2);
71 out.push('"');
72 for c in s.chars() {
73 if c == '"' || c == '\\' {
74 out.push('\\');
75 }
76 out.push(c);
77 }
78 out.push('"');
79 Ok(out)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn test_password_obfuscation() {
88 let pass = Password::new("secret_pass");
89 let debug_str = format!("{:?}", pass);
90 assert_eq!(debug_str, "\"***\"");
91 assert!(!debug_str.contains("secret_pass"));
92 assert_eq!(pass.as_str(), "secret_pass");
93 }
94
95 #[test]
96 fn test_oauth_obfuscation() {
97 let token = OAuthToken::new("ya29.token");
98 let debug_str = format!("{:?}", token);
99 assert_eq!(debug_str, "\"***\"");
100 assert!(!debug_str.contains("ya29.token"));
101 assert_eq!(token.as_str(), "ya29.token");
102 }
103
104 #[test]
105 fn test_imap_quoted_basic() {
106 assert_eq!(imap_quoted("hello").unwrap(), "\"hello\"");
107 assert_eq!(imap_quoted("").unwrap(), "\"\"");
108 }
109
110 #[test]
111 fn test_imap_quoted_escapes() {
112 assert_eq!(imap_quoted("a\"b").unwrap(), "\"a\\\"b\"");
113 assert_eq!(imap_quoted("a\\b").unwrap(), "\"a\\\\b\"");
114 assert_eq!(imap_quoted("a\\\"b").unwrap(), "\"a\\\\\\\"b\"");
115 }
116
117 #[test]
118 fn test_imap_quoted_rejects_cr_lf() {
119 assert!(imap_quoted("with\rCR").is_err());
120 assert!(imap_quoted("with\nLF").is_err());
121 }
122
123 #[test]
124 fn test_imap_quoted_rejects_nul_and_8bit() {
125 assert!(imap_quoted("a\0b").is_err());
126 assert!(imap_quoted("café").is_err());
127 }
128
129 #[test]
130 fn test_password_as_imap_quoted_roundtrip() {
131 let p = Password::new("p@ss\"with\\specials");
132 assert_eq!(p.as_imap_quoted().unwrap(), "\"p@ss\\\"with\\\\specials\"");
133 }
134}