Fix IMAP: import failed after authenticating to Dovecot when a capability contained :, such as IMAPSIEVE=sieve://... (fixes #43)
This commit is contained in:
@@ -82,6 +82,10 @@ service managesieve-login {
|
||||
}
|
||||
}
|
||||
|
||||
protocol imap {
|
||||
mail_plugins = $mail_plugins imap_sieve
|
||||
}
|
||||
|
||||
protocol sieve {
|
||||
managesieve_max_line_length = 1M
|
||||
managesieve_max_compile_errors = 5
|
||||
@@ -96,6 +100,7 @@ info_log_path = /dev/stderr
|
||||
plugin {
|
||||
sieve = file:~/sieve;active=~/.dovecot.sieve
|
||||
sieve_max_script_size = 1M
|
||||
imapsieve_url = sieve://127.0.0.1:4190
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -139,6 +144,7 @@ impl Dovecot {
|
||||
let host = container.get_host()?.to_string();
|
||||
let imap = Endpoint::new(host.clone(), container.get_host_port_ipv4(IMAP_PORT.tcp())?);
|
||||
let sieve = Endpoint::new(host, container.get_host_port_ipv4(SIEVE_PORT.tcp())?);
|
||||
ImapSeed::connect(&imap.host, imap.port)?.logout()?;
|
||||
|
||||
let accounts: Vec<Account> = layouts::accounts()
|
||||
.iter()
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Stalwart {
|
||||
.with_mapped_port(sieve_port, SIEVE_PORT.tcp())
|
||||
.with_startup_timeout(Duration::from_secs(180));
|
||||
|
||||
let container = request.start()?;
|
||||
let container = request.pull_image()?.start()?;
|
||||
|
||||
let me = Self {
|
||||
_container: container,
|
||||
|
||||
@@ -18,6 +18,9 @@ use integration::validate::{
|
||||
|
||||
use rusqlite::Connection;
|
||||
use vandelay::error::Error;
|
||||
use vandelay::imap::client::{ConnectMode, ImapClient};
|
||||
use vandelay::imap::transport::Connector;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||
use vandelay::sync::{import_imap, import_managesieve};
|
||||
@@ -561,3 +564,28 @@ fn dovecot_non_ascii_mailbox_names_round_trip() {
|
||||
);
|
||||
cleanup(&archive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires Docker"]
|
||||
fn dovecot_post_login_capability_with_imapsieve_url_is_parsed() {
|
||||
let d = Dovecot::start().expect("dovecot start");
|
||||
let account = d.accounts.first().expect("account");
|
||||
let connector = Connector::new(true).expect("connector");
|
||||
let mut client = ImapClient::connect(
|
||||
&connector,
|
||||
&d.imap.host,
|
||||
d.imap.port,
|
||||
ConnectMode::StartTls,
|
||||
Logger::from_flags(false, 0),
|
||||
)
|
||||
.expect("connect");
|
||||
assert!(client.has_capability("SASL-IR"));
|
||||
client
|
||||
.authenticate_plain(&account.username, &account.password)
|
||||
.expect("AUTHENTICATE PLAIN must survive the untagged post-login CAPABILITY");
|
||||
assert!(
|
||||
client.has_capability("IMAPSIEVE=sieve://127.0.0.1:4190"),
|
||||
"post-login capabilities must carry the IMAPSIEVE URL: {:?}",
|
||||
client.capabilities
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ fn get_item_mixed_success_and_per_item_error() {
|
||||
</m:GetItemResponseMessage>\
|
||||
</m:ResponseMessages></m:GetItemResponse>"
|
||||
));
|
||||
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
|
||||
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||
assert_eq!(r.len(), 2);
|
||||
assert!(r[0].success);
|
||||
assert!(r[0].inner_xml.contains("SGVsbG8="));
|
||||
@@ -312,7 +312,7 @@ fn mime_content_round_trips_through_base64_decode() {
|
||||
</t:Message></m:Items></m:GetItemResponseMessage>\
|
||||
</m:ResponseMessages></m:GetItemResponse>"
|
||||
));
|
||||
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
|
||||
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||
let item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
|
||||
let s = item.mime_content.unwrap();
|
||||
let bytes = LENIENT.decode(&s).unwrap();
|
||||
@@ -890,7 +890,7 @@ fn warning_response_class_is_treated_as_success_in_mock() {
|
||||
<m:Items><t:Message><t:ItemId Id=\"W1\" ChangeKey=\"K\"/></t:Message></m:Items>\
|
||||
</m:GetItemResponseMessage></m:ResponseMessages></m:GetItemResponse>"
|
||||
));
|
||||
let r = parse_response_messages(body.as_bytes(), b"GetItemResponseMessage").unwrap();
|
||||
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||
assert!(r[0].success);
|
||||
}
|
||||
|
||||
@@ -930,7 +930,7 @@ fn get_folder_messages_preserve_position_when_one_errors() {
|
||||
</m:GetFolderResponseMessage>\
|
||||
</m:ResponseMessages></m:GetFolderResponse>"
|
||||
));
|
||||
let msgs = parse_response_messages(body.as_bytes(), b"GetFolderResponseMessage").unwrap();
|
||||
let msgs = parse_response_messages(body.as_bytes(), "GetFolderResponseMessage").unwrap();
|
||||
assert_eq!(msgs.len(), 3, "all three messages must be present");
|
||||
assert!(msgs[0].success);
|
||||
assert!(!msgs[1].success);
|
||||
|
||||
+83
-17
@@ -487,26 +487,35 @@ fn write_fetch_message(
|
||||
conn.write_raw(b")\r\n")
|
||||
}
|
||||
|
||||
fn serve_one_folder(
|
||||
conn: &mut MockConn,
|
||||
uidvalidity: u32,
|
||||
uidnext: u32,
|
||||
uids: &[u32],
|
||||
) -> std::io::Result<()> {
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "LIST \"\" \"*\"");
|
||||
conn.write_line("* LIST () \"/\" \"INBOX\"")?;
|
||||
conn.write_line(&format!("{tag} OK LIST done"))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "LSUB \"\" \"*\"");
|
||||
conn.write_line(&format!("{tag} OK LSUB done"))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "SELECT \"INBOX\"");
|
||||
write_select(conn, &tag, uidvalidity, uidnext, uids.len() as u32)?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "UID SEARCH ALL");
|
||||
let uid_strs: Vec<String> = uids.iter().map(|u| u.to_string()).collect();
|
||||
conn.write_line(&format!("* SEARCH {}", uid_strs.join(" ")))?;
|
||||
conn.write_line(&format!("{tag} OK SEARCH done"))?;
|
||||
drain_until_close(conn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn control_script_one_folder(uidvalidity: u32, uidnext: u32, uids: &'static [u32]) -> Script {
|
||||
Box::new(move |conn: &mut MockConn| -> std::io::Result<()> {
|
||||
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "LIST \"\" \"*\"");
|
||||
conn.write_line("* LIST () \"/\" \"INBOX\"")?;
|
||||
conn.write_line(&format!("{tag} OK LIST done"))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "LSUB \"\" \"*\"");
|
||||
conn.write_line(&format!("{tag} OK LSUB done"))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "SELECT \"INBOX\"");
|
||||
write_select(conn, &tag, uidvalidity, uidnext, uids.len() as u32)?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "UID SEARCH ALL");
|
||||
let uid_strs: Vec<String> = uids.iter().map(|u| u.to_string()).collect();
|
||||
conn.write_line(&format!("* SEARCH {}", uid_strs.join(" ")))?;
|
||||
conn.write_line(&format!("{tag} OK SEARCH done"))?;
|
||||
drain_until_close(conn);
|
||||
Ok(())
|
||||
serve_one_folder(conn, uidvalidity, uidnext, uids)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -583,6 +592,63 @@ fn coordinator_imports_one_folder_one_message() {
|
||||
assert_eq!(folder_role(&conn, "INBOX"), Some("inbox".to_owned()));
|
||||
}
|
||||
|
||||
const DOVECOT_PRE_LOGIN_CAPS: &str =
|
||||
"IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ AUTH=PLAIN AUTH=LOGIN";
|
||||
const DOVECOT_POST_LOGIN_CAPS: &str = "IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT \
|
||||
UIDPLUS LITERAL+ NOTIFY IMAPSIEVE=sieve://127.0.0.1:4190 \
|
||||
QUOTA ACL RIGHTS=texk";
|
||||
|
||||
#[test]
|
||||
fn coordinator_accepts_dovecot_post_login_capability_with_imapsieve_url() {
|
||||
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
||||
conn.write_line(&format!(
|
||||
"* OK [CAPABILITY {DOVECOT_PRE_LOGIN_CAPS}] Dovecot (Debian) ready."
|
||||
))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "CAPABILITY");
|
||||
write_capability(conn, DOVECOT_PRE_LOGIN_CAPS)?;
|
||||
conn.write_line(&format!(
|
||||
"{tag} OK Pre-login capabilities listed, post-login capabilities have more."
|
||||
))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert!(
|
||||
cmd.starts_with("AUTHENTICATE PLAIN "),
|
||||
"expected SASL-IR form, got {cmd}"
|
||||
);
|
||||
write_capability(conn, DOVECOT_POST_LOGIN_CAPS)?;
|
||||
conn.write_line(&format!(
|
||||
"{tag} OK [CAPABILITY {DOVECOT_POST_LOGIN_CAPS}] Logged in"
|
||||
))?;
|
||||
let (tag, cmd) = conn.read_command()?;
|
||||
assert_eq!(cmd, "CAPABILITY");
|
||||
write_capability(conn, DOVECOT_POST_LOGIN_CAPS)?;
|
||||
conn.write_line(&format!("{tag} OK Capability completed."))?;
|
||||
serve_one_folder(conn, 12345, 2, &[1])
|
||||
});
|
||||
let server = MockImap::start_scripts(vec![
|
||||
control,
|
||||
worker_fetch_script(
|
||||
"IMAP4rev1 SASL-IR LITERAL+ AUTH=PLAIN IMAPSIEVE=sieve://127.0.0.1:4190",
|
||||
"INBOX",
|
||||
12345,
|
||||
2,
|
||||
1,
|
||||
vec![(1, 1, MSG_BODY)],
|
||||
),
|
||||
]);
|
||||
let archive = tempfile("dovecot-imapsieve");
|
||||
let summary = run_import(&server, "alice", archive.clone(), |_| {}).expect("import");
|
||||
let email = summary
|
||||
.per_type
|
||||
.iter()
|
||||
.find(|(k, _)| *k == "email")
|
||||
.unwrap();
|
||||
assert_eq!(email.1.created, 1, "summary={summary:?}");
|
||||
let conn = Connection::open(&archive).unwrap();
|
||||
db::init::apply_schema(&conn).unwrap();
|
||||
assert_eq!(count(&conn, "emails"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coordinator_uses_esearch_when_advertised() {
|
||||
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
||||
|
||||
@@ -197,7 +197,7 @@ impl Admin {
|
||||
"encryptionAtRest": { "@type": "Disabled" },
|
||||
"permissions": { "@type": "Inherit" },
|
||||
"roles": { "@type": role },
|
||||
"locale": "en_US"
|
||||
"locale": "en-US"
|
||||
}
|
||||
});
|
||||
let response =
|
||||
|
||||
+9
-1
@@ -1243,6 +1243,10 @@ fn apply_jmap_settings(
|
||||
.is_some(),
|
||||
"x:Jmap/set not applied: {resp}"
|
||||
);
|
||||
assert!(
|
||||
resp.pointer("/methodResponses/1/1/created/r").is_some(),
|
||||
"ReloadSettings failed, so the x:Jmap/set values are not in effect: {resp}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1255,8 +1259,13 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
||||
let fx = seeder::provision(base_url()).expect("provision");
|
||||
let acc = fx.account("test1").expect("test1");
|
||||
|
||||
let blob_size = 8 * 1024 * 1024;
|
||||
let mut updates = serde_json::Map::new();
|
||||
updates.insert("uploadTtl".to_owned(), serde_json::json!(5_000));
|
||||
updates.insert(
|
||||
"uploadQuota".to_owned(),
|
||||
serde_json::json!(blob_size * 5 / 2),
|
||||
);
|
||||
let _ttl_guard = JmapSettingsGuard::override_settings(updates);
|
||||
|
||||
let client = HttpClient::new(basic("test1"), RetryPolicy::new(20), true);
|
||||
@@ -1264,7 +1273,6 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
||||
let limits = session.core_limits().expect("core limits");
|
||||
client.set_limits(&limits);
|
||||
|
||||
let blob_size = 8 * 1024 * 1024;
|
||||
let mut blob = vec![0u8; blob_size];
|
||||
let max_uploads = 8u32;
|
||||
let mut accepted = 0u32;
|
||||
|
||||
Reference in New Issue
Block a user