max / goingson
4 files changed,
+38 insertions,
-27 deletions
| @@ -208,10 +208,13 @@ impl ImapClient { | |||
| 208 | 208 | continue; | |
| 209 | 209 | } | |
| 210 | 210 | }; | |
| 211 | - | let uid = message.uid.unwrap_or_else(|| { | |
| 212 | - | tracing::warn!(folder = %folder_name, "IMAP message missing UID, using 0"); | |
| 213 | - | 0 | |
| 214 | - | }); | |
| 211 | + | let uid = match message.uid { | |
| 212 | + | Some(u) => u, | |
| 213 | + | None => { | |
| 214 | + | tracing::warn!(folder = %folder_name, "IMAP message missing UID, skipping"); | |
| 215 | + | continue; | |
| 216 | + | } | |
| 217 | + | }; | |
| 215 | 218 | ||
| 216 | 219 | // Check if \Seen flag is present (email has been read) | |
| 217 | 220 | let is_read = message.flags().any(|f| { | |
| @@ -338,7 +341,7 @@ impl ImapClient { | |||
| 338 | 341 | ||
| 339 | 342 | let uids: Vec<u32> = if use_uid { | |
| 340 | 343 | let state = sync_state.unwrap(); | |
| 341 | - | let search_query = format!("UID {}:*", state.last_seen_uid + 1); | |
| 344 | + | let search_query = format!("UID {}:*", state.last_seen_uid.saturating_add(1)); | |
| 342 | 345 | debug.push(format!("uid_search: {}", search_query)); | |
| 343 | 346 | ||
| 344 | 347 | let search_result = session | |
| @@ -409,14 +412,15 @@ impl ImapClient { | |||
| 409 | 412 | continue; | |
| 410 | 413 | } | |
| 411 | 414 | }; | |
| 412 | - | let uid = message.uid.unwrap_or_else(|| { | |
| 413 | - | tracing::warn!(folder = %folder_name, "IMAP message missing UID, using 0"); | |
| 414 | - | 0 | |
| 415 | - | }); | |
| 415 | + | let uid = match message.uid { | |
| 416 | + | Some(u) => u, | |
| 417 | + | None => { | |
| 418 | + | tracing::warn!(folder = %folder_name, "IMAP message missing UID, skipping"); | |
| 419 | + | continue; | |
| 420 | + | } | |
| 421 | + | }; | |
| 416 | 422 | ||
| 417 | - | if uid > 0 { | |
| 418 | - | max_uid = Some(max_uid.map_or(uid, |m: u32| m.max(uid))); | |
| 419 | - | } | |
| 423 | + | max_uid = Some(max_uid.map_or(uid, |m: u32| m.max(uid))); | |
| 420 | 424 | ||
| 421 | 425 | let is_read = message.flags().any(|f| matches!(f, async_imap::types::Flag::Seen)); | |
| 422 | 426 | ||
| @@ -674,6 +678,8 @@ impl ImapClient { | |||
| 674 | 678 | if let Some(end) = text.to_lowercase()[start..].find("</script>") { | |
| 675 | 679 | text = format!("{}{}", &text[..start], &text[start + end + 9..]); | |
| 676 | 680 | } else { | |
| 681 | + | // Unclosed script tag — remove everything from here to end | |
| 682 | + | text.truncate(start); | |
| 677 | 683 | break; | |
| 678 | 684 | } | |
| 679 | 685 | } | |
| @@ -683,6 +689,7 @@ impl ImapClient { | |||
| 683 | 689 | if let Some(end) = text.to_lowercase()[start..].find("</style>") { | |
| 684 | 690 | text = format!("{}{}", &text[..start], &text[start + end + 8..]); | |
| 685 | 691 | } else { | |
| 692 | + | text.truncate(start); | |
| 686 | 693 | break; | |
| 687 | 694 | } | |
| 688 | 695 | } |
| @@ -48,7 +48,7 @@ impl From<ParsedEmail> for UnifiedEmail { | |||
| 48 | 48 | subject: email.subject, | |
| 49 | 49 | body: email.body, | |
| 50 | 50 | date: email.date, | |
| 51 | - | is_read: false, // IMAP fetch doesn't include flags | |
| 51 | + | is_read: email.is_read, | |
| 52 | 52 | } | |
| 53 | 53 | } | |
| 54 | 54 | } |
| @@ -27,7 +27,7 @@ impl JmapClient { | |||
| 27 | 27 | .timeout(Duration::from_secs(30)) | |
| 28 | 28 | .connect_timeout(Duration::from_secs(10)) | |
| 29 | 29 | .build() | |
| 30 | - | .unwrap_or_default(); | |
| 30 | + | .expect("failed to build HTTP client"); | |
| 31 | 31 | Self { | |
| 32 | 32 | client, | |
| 33 | 33 | access_token: access_token.into(), | |
| @@ -46,7 +46,7 @@ impl JmapClient { | |||
| 46 | 46 | .timeout(Duration::from_secs(30)) | |
| 47 | 47 | .connect_timeout(Duration::from_secs(10)) | |
| 48 | 48 | .build() | |
| 49 | - | .unwrap_or_default(); | |
| 49 | + | .expect("failed to build HTTP client"); | |
| 50 | 50 | Self { | |
| 51 | 51 | client, | |
| 52 | 52 | access_token: access_token.into(), |
| @@ -155,7 +155,7 @@ impl OAuthCallbackServer { | |||
| 155 | 155 | stored: &Arc<Mutex<StoredCallback>>, | |
| 156 | 156 | _callback_received: bool, | |
| 157 | 157 | ) -> Option<Result<CallbackResult, CallbackError>> { | |
| 158 | - | let mut buffer = [0; 4096]; | |
| 158 | + | let mut buffer = [0; 16384]; | |
| 159 | 159 | let n = stream.read(&mut buffer).ok()?; | |
| 160 | 160 | let request = String::from_utf8_lossy(&buffer[..n]); | |
| 161 | 161 | ||
| @@ -247,15 +247,17 @@ impl OAuthCallbackServer { | |||
| 247 | 247 | })); | |
| 248 | 248 | } | |
| 249 | 249 | ||
| 250 | - | // Extract code and state | |
| 250 | + | // Extract code and state, URL-decoding to handle encoded chars | |
| 251 | 251 | let code = params.get("code")?; | |
| 252 | 252 | let state = params.get("state")?; | |
| 253 | + | let code = urlencoding::decode(code).unwrap_or_else(|_| code.to_string()); | |
| 254 | + | let state = urlencoding::decode(state).unwrap_or_else(|_| state.to_string()); | |
| 253 | 255 | ||
| 254 | 256 | // Store the success result | |
| 255 | 257 | if let Ok(mut stored_guard) = stored.lock() { | |
| 256 | 258 | *stored_guard = StoredCallback::Success { | |
| 257 | - | code: code.to_string(), | |
| 258 | - | state: state.to_string(), | |
| 259 | + | code: code.clone(), | |
| 260 | + | state: state.clone(), | |
| 259 | 261 | }; | |
| 260 | 262 | } | |
| 261 | 263 | ||
| @@ -276,8 +278,8 @@ impl OAuthCallbackServer { | |||
| 276 | 278 | ); | |
| 277 | 279 | ||
| 278 | 280 | Some(Ok(CallbackResult { | |
| 279 | - | code: code.to_string(), | |
| 280 | - | state: state.to_string(), | |
| 281 | + | code, | |
| 282 | + | state, | |
| 281 | 283 | })) | |
| 282 | 284 | } | |
| 283 | 285 | ||
| @@ -298,10 +300,10 @@ impl OAuthCallbackServer { | |||
| 298 | 300 | } | |
| 299 | 301 | } | |
| 300 | 302 | ||
| 301 | - | /// Simple URL decoding. | |
| 303 | + | /// Simple URL decoding with proper multi-byte UTF-8 support. | |
| 302 | 304 | mod urlencoding { | |
| 303 | 305 | pub fn decode(s: &str) -> Result<String, ()> { | |
| 304 | - | let mut result = String::with_capacity(s.len()); | |
| 306 | + | let mut bytes = Vec::with_capacity(s.len()); | |
| 305 | 307 | let mut chars = s.chars(); | |
| 306 | 308 | ||
| 307 | 309 | while let Some(c) = chars.next() { | |
| @@ -309,19 +311,21 @@ mod urlencoding { | |||
| 309 | 311 | let hex: String = chars.by_ref().take(2).collect(); | |
| 310 | 312 | if hex.len() == 2 { | |
| 311 | 313 | if let Ok(byte) = u8::from_str_radix(&hex, 16) { | |
| 312 | - | result.push(byte as char); | |
| 314 | + | bytes.push(byte); | |
| 313 | 315 | continue; | |
| 314 | 316 | } | |
| 315 | 317 | } | |
| 316 | 318 | return Err(()); | |
| 317 | 319 | } else if c == '+' { | |
| 318 | - | result.push(' '); | |
| 320 | + | bytes.push(b' '); | |
| 319 | 321 | } else { | |
| 320 | - | result.push(c); | |
| 322 | + | let mut buf = [0u8; 4]; | |
| 323 | + | let encoded = c.encode_utf8(&mut buf); | |
| 324 | + | bytes.extend_from_slice(encoded.as_bytes()); | |
| 321 | 325 | } | |
| 322 | 326 | } | |
| 323 | 327 | ||
| 324 | - | Ok(result) | |
| 328 | + | String::from_utf8(bytes).map_err(|_| ()) | |
| 325 | 329 | } | |
| 326 | 330 | } | |
| 327 | 331 |