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