import: keep what an interrupted IMAP or Maildir import wrote
IMAP import wrote a whole folder in one transaction and stopped the folder at the first message it could not import. A crash near the end of a large INBOX kept nothing, and one bad INTERNALDATE lost the rest of the folder. Worse, when a folder stopped early, fetches still in flight for it could be filed into the next folder's mailbox. - The transaction is committed after every fetch chunk. Each message is written in its own savepoint, so what is committed is always whole, and a rerun fetches only the UIDs still missing. - A message that cannot be imported is rolled back on its own, logged with its folder and UID, and counted as failed; the folder carries on, and the message stays out of the UID map so the next run tries it again. Archive and I/O errors still stop the run. - Fetch jobs and events carry a folder generation. Moving to a new folder cancels queued work for older ones, and any event from an older generation is dropped, never filed. Shutdown drains in-flight events before joining the workers, so it cannot hang on a blocked worker. - INTERNALDATE month names are matched in any case. - Maildir import gets the same per-message savepoint, and commits every 500 new messages instead of once per folder.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -58,20 +59,22 @@ pub fn imap_internaldate_to_rfc3339(s: &str) -> Result<String, Error> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// RFC 3501 spells the month "Jan", but servers are not all that careful,
|
||||
// and a date is not worth losing a message over: match any case.
|
||||
fn month_to_num(s: &str) -> Result<u32, Error> {
|
||||
let m = match s {
|
||||
"Jan" => 1,
|
||||
"Feb" => 2,
|
||||
"Mar" => 3,
|
||||
"Apr" => 4,
|
||||
"May" => 5,
|
||||
"Jun" => 6,
|
||||
"Jul" => 7,
|
||||
"Aug" => 8,
|
||||
"Sep" => 9,
|
||||
"Oct" => 10,
|
||||
"Nov" => 11,
|
||||
"Dec" => 12,
|
||||
let m = match s.to_ascii_lowercase().as_str() {
|
||||
"jan" => 1,
|
||||
"feb" => 2,
|
||||
"mar" => 3,
|
||||
"apr" => 4,
|
||||
"may" => 5,
|
||||
"jun" => 6,
|
||||
"jul" => 7,
|
||||
"aug" => 8,
|
||||
"sep" => 9,
|
||||
"oct" => 10,
|
||||
"nov" => 11,
|
||||
"dec" => 12,
|
||||
other => return Err(Error::Partial(format!("INTERNALDATE month {other:?}"))),
|
||||
};
|
||||
Ok(m)
|
||||
@@ -99,6 +102,22 @@ fn parse_zone(s: &str) -> Result<(char, u32, u32), Error> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn month_matches_any_case() {
|
||||
for d in [
|
||||
"12-May-2025 10:00:00 +0000",
|
||||
"12-may-2025 10:00:00 +0000",
|
||||
"12-MAY-2025 10:00:00 +0000",
|
||||
] {
|
||||
assert_eq!(
|
||||
imap_internaldate_to_rfc3339(d).unwrap(),
|
||||
"2025-05-12T10:00:00Z",
|
||||
"{d}"
|
||||
);
|
||||
}
|
||||
assert!(imap_internaldate_to_rfc3339("12-Mai-2025 10:00:00 +0000").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utc_zone_becomes_z() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user