Fix IMAP: import failed after authenticating to Dovecot when a capability contained :, such as IMAPSIEVE=sieve://... (fixes #43)
This commit is contained in:
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [1.0.11] - 2026-09-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Replaced `base64` with `encodify`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- IMAP: import failed after authenticating to Dovecot when a capability contained `:`, such as `IMAPSIEVE=sieve://...` (fixes #43)
|
||||||
|
|
||||||
## [1.0.10] - 2026-08-27
|
## [1.0.10] - 2026-08-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+4
-2
@@ -724,6 +724,8 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "encodify"
|
name = "encodify"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fda1e5c788c2b59096e152ad1a76ce72a2ab80c75985eb94ca82de0b6555db7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"simdutf8",
|
"simdutf8",
|
||||||
@@ -1936,9 +1938,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.41.0"
|
version = "0.42.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-5
@@ -26,7 +26,7 @@ bundled-sqlite = ["rusqlite/bundled"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
indexmap = { version = "2.14", features = ["serde"] }
|
||||||
rusqlite = { version = "0.40", features = ["blob"] }
|
rusqlite = { version = "0.40", features = ["blob"] }
|
||||||
blake3 = "1.8.5"
|
blake3 = "1.8.5"
|
||||||
time = { version = "0.3.47", features = ["serde-well-known", "macros"] }
|
time = { version = "0.3.47", features = ["serde-well-known", "macros"] }
|
||||||
@@ -43,7 +43,7 @@ rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs
|
|||||||
rustls-platform-verifier = "0.7"
|
rustls-platform-verifier = "0.7"
|
||||||
regex = "1.13"
|
regex = "1.13"
|
||||||
flate2 = "1.1.9"
|
flate2 = "1.1.9"
|
||||||
quick-xml = "0.41"
|
quick-xml = "0.42"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
chrono-tz = { version = "0.10", default-features = false }
|
chrono-tz = { version = "0.10", default-features = false }
|
||||||
psl = "2.1.226"
|
psl = "2.1.226"
|
||||||
@@ -56,6 +56,3 @@ testcontainers = { version = "0.28", features = ["blocking"] }
|
|||||||
[profile.dist]
|
[profile.dist]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
encodify = { path = "../encodify" }
|
|
||||||
|
|||||||
+72
-78
@@ -78,13 +78,13 @@ fn classify_ns(ns: &ResolveResult<'_>) -> NsKind {
|
|||||||
match ns {
|
match ns {
|
||||||
ResolveResult::Bound(prefix) => {
|
ResolveResult::Bound(prefix) => {
|
||||||
let p = prefix.as_ref();
|
let p = prefix.as_ref();
|
||||||
if p == NS_DAV.as_bytes() {
|
if p == NS_DAV {
|
||||||
NsKind::Dav
|
NsKind::Dav
|
||||||
} else if p == NS_CALDAV.as_bytes() {
|
} else if p == NS_CALDAV {
|
||||||
NsKind::Caldav
|
NsKind::Caldav
|
||||||
} else if p == NS_CARDDAV.as_bytes() {
|
} else if p == NS_CARDDAV {
|
||||||
NsKind::Carddav
|
NsKind::Carddav
|
||||||
} else if p == NS_APPLE_ICAL.as_bytes() {
|
} else if p == NS_APPLE_ICAL {
|
||||||
NsKind::Apple
|
NsKind::Apple
|
||||||
} else {
|
} else {
|
||||||
NsKind::Other
|
NsKind::Other
|
||||||
@@ -94,7 +94,7 @@ fn classify_ns(ns: &ResolveResult<'_>) -> NsKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is(ns: NsKind, local: &[u8], expected_ns: NsKind, target: &[u8]) -> bool {
|
fn is(ns: NsKind, local: &str, expected_ns: NsKind, target: &str) -> bool {
|
||||||
ns == expected_ns && local.eq_ignore_ascii_case(target)
|
ns == expected_ns && local.eq_ignore_ascii_case(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,17 +102,17 @@ fn is(ns: NsKind, local: &[u8], expected_ns: NsKind, target: &[u8]) -> bool {
|
|||||||
enum Step {
|
enum Step {
|
||||||
StartElement {
|
StartElement {
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: Vec<u8>,
|
local: String,
|
||||||
attrs: Vec<(Vec<u8>, Vec<u8>)>,
|
attrs: Vec<(String, String)>,
|
||||||
},
|
},
|
||||||
EmptyElement {
|
EmptyElement {
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: Vec<u8>,
|
local: String,
|
||||||
attrs: Vec<(Vec<u8>, Vec<u8>)>,
|
attrs: Vec<(String, String)>,
|
||||||
},
|
},
|
||||||
EndElement {
|
EndElement {
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: Vec<u8>,
|
local: String,
|
||||||
},
|
},
|
||||||
Text(String),
|
Text(String),
|
||||||
CData(String),
|
CData(String),
|
||||||
@@ -125,7 +125,7 @@ fn resolve_entity(g: &BytesRef) -> Option<char> {
|
|||||||
if let Ok(Some(c)) = g.resolve_char_ref() {
|
if let Ok(Some(c)) = g.resolve_char_ref() {
|
||||||
return Some(c);
|
return Some(c);
|
||||||
}
|
}
|
||||||
match g.decode().ok()?.as_ref() {
|
match g.as_ref() {
|
||||||
"amp" => Some('&'),
|
"amp" => Some('&'),
|
||||||
"lt" => Some('<'),
|
"lt" => Some('<'),
|
||||||
"gt" => Some('>'),
|
"gt" => Some('>'),
|
||||||
@@ -141,11 +141,12 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
|
|||||||
let ns_kind = classify_ns(&ns);
|
let ns_kind = classify_ns(&ns);
|
||||||
Ok(match ev {
|
Ok(match ev {
|
||||||
Event::Start(e) => {
|
Event::Start(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
let mut attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
let attrs = e
|
||||||
for a in e.attributes().flatten() {
|
.attributes()
|
||||||
attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec()));
|
.flatten()
|
||||||
}
|
.map(|a| (a.key.as_ref().to_owned(), a.value.into_owned()))
|
||||||
|
.collect();
|
||||||
Step::StartElement {
|
Step::StartElement {
|
||||||
ns: ns_kind,
|
ns: ns_kind,
|
||||||
local,
|
local,
|
||||||
@@ -153,11 +154,12 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Empty(e) => {
|
Event::Empty(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
let mut attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
|
let attrs = e
|
||||||
for a in e.attributes().flatten() {
|
.attributes()
|
||||||
attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec()));
|
.flatten()
|
||||||
}
|
.map(|a| (a.key.as_ref().to_owned(), a.value.into_owned()))
|
||||||
|
.collect();
|
||||||
Step::EmptyElement {
|
Step::EmptyElement {
|
||||||
ns: ns_kind,
|
ns: ns_kind,
|
||||||
local,
|
local,
|
||||||
@@ -166,13 +168,10 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
|
|||||||
}
|
}
|
||||||
Event::End(e) => Step::EndElement {
|
Event::End(e) => Step::EndElement {
|
||||||
ns: ns_kind,
|
ns: ns_kind,
|
||||||
local: e.local_name().as_ref().to_vec(),
|
local: e.local_name().as_ref().to_owned(),
|
||||||
},
|
},
|
||||||
Event::Text(t) => {
|
Event::Text(t) => Step::Text(t.into_inner().into_owned()),
|
||||||
let s = t.decode().map_err(|e| ParseError::Xml(e.to_string()))?;
|
Event::CData(cd) => Step::CData(cd.into_inner().into_owned()),
|
||||||
Step::Text(s.into_owned())
|
|
||||||
}
|
|
||||||
Event::CData(cd) => Step::CData(String::from_utf8_lossy(cd.as_ref()).into_owned()),
|
|
||||||
Event::GeneralRef(g) => match resolve_entity(&g) {
|
Event::GeneralRef(g) => match resolve_entity(&g) {
|
||||||
Some(c) => Step::Entity(c),
|
Some(c) => Step::Entity(c),
|
||||||
None => Step::Other,
|
None => Step::Other,
|
||||||
@@ -194,7 +193,7 @@ pub fn parse_multistatus<R: BufRead>(
|
|||||||
loop {
|
loop {
|
||||||
let step = next_step(&mut xml, &mut buf)?;
|
let step = next_step(&mut xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } if is(ns, &local, NsKind::Dav, b"response") => {
|
Step::StartElement { ns, local, .. } if is(ns, &local, NsKind::Dav, "response") => {
|
||||||
if let Some(r) = parse_response(&mut xml, base_url)?
|
if let Some(r) = parse_response(&mut xml, base_url)?
|
||||||
&& seen.insert(r.href.as_str().to_owned())
|
&& seen.insert(r.href.as_str().to_owned())
|
||||||
{
|
{
|
||||||
@@ -222,15 +221,15 @@ fn parse_response<R: BufRead>(
|
|||||||
let step = next_step(xml, &mut buf)?;
|
let step = next_step(xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } => {
|
Step::StartElement { ns, local, .. } => {
|
||||||
if is(ns, &local, NsKind::Dav, b"href") {
|
if is(ns, &local, NsKind::Dav, "href") {
|
||||||
let text = read_token(xml)?;
|
let text = read_token(xml)?;
|
||||||
if href.is_none() {
|
if href.is_none() {
|
||||||
href = Some(text);
|
href = Some(text);
|
||||||
}
|
}
|
||||||
} else if is(ns, &local, NsKind::Dav, b"status") {
|
} else if is(ns, &local, NsKind::Dav, "status") {
|
||||||
let text = read_token(xml)?;
|
let text = read_token(xml)?;
|
||||||
status = parse_http_status(&text);
|
status = parse_http_status(&text);
|
||||||
} else if is(ns, &local, NsKind::Dav, b"propstat") {
|
} else if is(ns, &local, NsKind::Dav, "propstat") {
|
||||||
let (block_status, block_props) = parse_propstat(xml)?;
|
let (block_status, block_props) = parse_propstat(xml)?;
|
||||||
match block_status {
|
match block_status {
|
||||||
Some(s) if (200..300).contains(&s) => merge_props(&mut props, block_props),
|
Some(s) if (200..300).contains(&s) => merge_props(&mut props, block_props),
|
||||||
@@ -241,7 +240,7 @@ fn parse_response<R: BufRead>(
|
|||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"response") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "response") => break,
|
||||||
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <response>".into())),
|
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <response>".into())),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -270,16 +269,16 @@ fn parse_propstat<R: BufRead>(
|
|||||||
let step = next_step(xml, &mut buf)?;
|
let step = next_step(xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } => {
|
Step::StartElement { ns, local, .. } => {
|
||||||
if is(ns, &local, NsKind::Dav, b"status") {
|
if is(ns, &local, NsKind::Dav, "status") {
|
||||||
let text = read_token(xml)?;
|
let text = read_token(xml)?;
|
||||||
status = parse_http_status(&text);
|
status = parse_http_status(&text);
|
||||||
} else if is(ns, &local, NsKind::Dav, b"prop") {
|
} else if is(ns, &local, NsKind::Dav, "prop") {
|
||||||
parse_prop_block(xml, &mut props)?;
|
parse_prop_block(xml, &mut props)?;
|
||||||
} else {
|
} else {
|
||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"propstat") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "propstat") => break,
|
||||||
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <propstat>".into())),
|
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <propstat>".into())),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -299,7 +298,7 @@ fn parse_prop_block<R: BufRead>(
|
|||||||
handle_prop_element(xml, props, ns, &local)?;
|
handle_prop_element(xml, props, ns, &local)?;
|
||||||
}
|
}
|
||||||
Step::EmptyElement { .. } => {}
|
Step::EmptyElement { .. } => {}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"prop") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "prop") => break,
|
||||||
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <prop>".into())),
|
Step::Eof => return Err(ParseError::Xml("unexpected EOF in <prop>".into())),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -311,53 +310,53 @@ fn handle_prop_element<R: BufRead>(
|
|||||||
xml: &mut NsReader<R>,
|
xml: &mut NsReader<R>,
|
||||||
props: &mut ResourceProps,
|
props: &mut ResourceProps,
|
||||||
ns: NsKind,
|
ns: NsKind,
|
||||||
local: &[u8],
|
local: &str,
|
||||||
) -> Result<(), ParseError> {
|
) -> Result<(), ParseError> {
|
||||||
if is(ns, local, NsKind::Dav, b"resourcetype") {
|
if is(ns, local, NsKind::Dav, "resourcetype") {
|
||||||
consume_resourcetype(xml, props)?;
|
consume_resourcetype(xml, props)?;
|
||||||
} else if is(ns, local, NsKind::Dav, b"displayname") {
|
} else if is(ns, local, NsKind::Dav, "displayname") {
|
||||||
props.displayname = Some(read_token(xml)?);
|
props.displayname = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"current-user-principal") {
|
} else if is(ns, local, NsKind::Dav, "current-user-principal") {
|
||||||
props.current_user_principal = read_first_href(xml, b"current-user-principal")?;
|
props.current_user_principal = read_first_href(xml, "current-user-principal")?;
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-home-set") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-home-set") {
|
||||||
props.calendar_home_set = read_first_href(xml, b"calendar-home-set")?;
|
props.calendar_home_set = read_first_href(xml, "calendar-home-set")?;
|
||||||
} else if is(ns, local, NsKind::Carddav, b"addressbook-home-set") {
|
} else if is(ns, local, NsKind::Carddav, "addressbook-home-set") {
|
||||||
props.addressbook_home_set = read_first_href(xml, b"addressbook-home-set")?;
|
props.addressbook_home_set = read_first_href(xml, "addressbook-home-set")?;
|
||||||
} else if is(ns, local, NsKind::Dav, b"getetag") {
|
} else if is(ns, local, NsKind::Dav, "getetag") {
|
||||||
let raw = read_token(xml)?;
|
let raw = read_token(xml)?;
|
||||||
if !raw.is_empty() {
|
if !raw.is_empty() {
|
||||||
props.etag = Some(raw);
|
props.etag = Some(raw);
|
||||||
}
|
}
|
||||||
} else if is(ns, local, NsKind::Dav, b"getcontenttype") {
|
} else if is(ns, local, NsKind::Dav, "getcontenttype") {
|
||||||
props.content_type = Some(read_token(xml)?);
|
props.content_type = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"getlastmodified") {
|
} else if is(ns, local, NsKind::Dav, "getlastmodified") {
|
||||||
props.last_modified = Some(read_token(xml)?);
|
props.last_modified = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"creationdate") {
|
} else if is(ns, local, NsKind::Dav, "creationdate") {
|
||||||
props.creation_date = Some(read_token(xml)?);
|
props.creation_date = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Dav, b"getcontentlength") {
|
} else if is(ns, local, NsKind::Dav, "getcontentlength") {
|
||||||
let raw = read_token(xml)?;
|
let raw = read_token(xml)?;
|
||||||
props.content_length = raw.parse::<u64>().ok();
|
props.content_length = raw.parse::<u64>().ok();
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-description") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-description") {
|
||||||
props.calendar_description = Some(read_token(xml)?);
|
props.calendar_description = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Carddav, b"addressbook-description") {
|
} else if is(ns, local, NsKind::Carddav, "addressbook-description") {
|
||||||
props.addressbook_description = Some(read_token(xml)?);
|
props.addressbook_description = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Apple, b"calendar-color") {
|
} else if is(ns, local, NsKind::Apple, "calendar-color") {
|
||||||
props.calendar_color = Some(read_token(xml)?);
|
props.calendar_color = Some(read_token(xml)?);
|
||||||
} else if is(ns, local, NsKind::Apple, b"calendar-order") {
|
} else if is(ns, local, NsKind::Apple, "calendar-order") {
|
||||||
let raw = read_token(xml)?;
|
let raw = read_token(xml)?;
|
||||||
props.calendar_order = raw.parse::<i64>().ok();
|
props.calendar_order = raw.parse::<i64>().ok();
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-timezone") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-timezone") {
|
||||||
props.calendar_timezone = Some(read_token(xml)?);
|
props.calendar_timezone = Some(read_token(xml)?);
|
||||||
} else if is(
|
} else if is(
|
||||||
ns,
|
ns,
|
||||||
local,
|
local,
|
||||||
NsKind::Caldav,
|
NsKind::Caldav,
|
||||||
b"supported-calendar-component-set",
|
"supported-calendar-component-set",
|
||||||
) {
|
) {
|
||||||
consume_supported_components(xml, props)?;
|
consume_supported_components(xml, props)?;
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar-data") {
|
} else if is(ns, local, NsKind::Caldav, "calendar-data") {
|
||||||
props.calendar_data = Some(read_text(xml)?);
|
props.calendar_data = Some(read_text(xml)?);
|
||||||
} else if is(ns, local, NsKind::Carddav, b"address-data") {
|
} else if is(ns, local, NsKind::Carddav, "address-data") {
|
||||||
props.address_data = Some(read_text(xml)?);
|
props.address_data = Some(read_text(xml)?);
|
||||||
} else {
|
} else {
|
||||||
skip_element(xml, local)?;
|
skip_element(xml, local)?;
|
||||||
@@ -380,7 +379,7 @@ fn consume_resourcetype<R: BufRead>(
|
|||||||
Step::EmptyElement { ns, local, .. } => {
|
Step::EmptyElement { ns, local, .. } => {
|
||||||
mark_resourcetype(ns, &local, props);
|
mark_resourcetype(ns, &local, props);
|
||||||
}
|
}
|
||||||
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, b"resourcetype") => break,
|
Step::EndElement { ns, local } if is(ns, &local, NsKind::Dav, "resourcetype") => break,
|
||||||
Step::Eof => {
|
Step::Eof => {
|
||||||
return Err(ParseError::Xml("unexpected EOF in <resourcetype>".into()));
|
return Err(ParseError::Xml("unexpected EOF in <resourcetype>".into()));
|
||||||
}
|
}
|
||||||
@@ -390,12 +389,12 @@ fn consume_resourcetype<R: BufRead>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_resourcetype(ns: NsKind, local: &[u8], props: &mut ResourceProps) {
|
fn mark_resourcetype(ns: NsKind, local: &str, props: &mut ResourceProps) {
|
||||||
if is(ns, local, NsKind::Dav, b"collection") {
|
if is(ns, local, NsKind::Dav, "collection") {
|
||||||
props.is_collection = true;
|
props.is_collection = true;
|
||||||
} else if is(ns, local, NsKind::Caldav, b"calendar") {
|
} else if is(ns, local, NsKind::Caldav, "calendar") {
|
||||||
props.is_calendar = true;
|
props.is_calendar = true;
|
||||||
} else if is(ns, local, NsKind::Carddav, b"addressbook") {
|
} else if is(ns, local, NsKind::Carddav, "addressbook") {
|
||||||
props.is_addressbook = true;
|
props.is_addressbook = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,8 +411,8 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
ns,
|
ns,
|
||||||
local,
|
local,
|
||||||
ref attrs,
|
ref attrs,
|
||||||
} if is(ns, &local, NsKind::Caldav, b"comp") => {
|
} if is(ns, &local, NsKind::Caldav, "comp") => {
|
||||||
if let Some(name) = attr_lookup(attrs, b"name") {
|
if let Some(name) = attr_lookup(attrs, "name") {
|
||||||
props.supported_components.push(name);
|
props.supported_components.push(name);
|
||||||
}
|
}
|
||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
@@ -422,8 +421,8 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
ns,
|
ns,
|
||||||
local,
|
local,
|
||||||
ref attrs,
|
ref attrs,
|
||||||
} if is(ns, &local, NsKind::Caldav, b"comp") => {
|
} if is(ns, &local, NsKind::Caldav, "comp") => {
|
||||||
if let Some(name) = attr_lookup(attrs, b"name") {
|
if let Some(name) = attr_lookup(attrs, "name") {
|
||||||
props.supported_components.push(name);
|
props.supported_components.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,7 +431,7 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
ns,
|
ns,
|
||||||
&local,
|
&local,
|
||||||
NsKind::Caldav,
|
NsKind::Caldav,
|
||||||
b"supported-calendar-component-set",
|
"supported-calendar-component-set",
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -448,13 +447,8 @@ fn consume_supported_components<R: BufRead>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attr_lookup(attrs: &[(Vec<u8>, Vec<u8>)], key: &[u8]) -> Option<String> {
|
fn attr_lookup(attrs: &[(String, String)], key: &str) -> Option<String> {
|
||||||
for (k, v) in attrs {
|
attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
|
||||||
if k == key {
|
|
||||||
return Some(String::from_utf8_lossy(v).into_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_text<R: BufRead>(xml: &mut NsReader<R>) -> Result<String, ParseError> {
|
fn read_text<R: BufRead>(xml: &mut NsReader<R>) -> Result<String, ParseError> {
|
||||||
@@ -480,7 +474,7 @@ fn read_token<R: BufRead>(xml: &mut NsReader<R>) -> Result<String, ParseError> {
|
|||||||
|
|
||||||
fn read_first_href<R: BufRead>(
|
fn read_first_href<R: BufRead>(
|
||||||
xml: &mut NsReader<R>,
|
xml: &mut NsReader<R>,
|
||||||
closing_local: &[u8],
|
closing_local: &str,
|
||||||
) -> Result<Option<String>, ParseError> {
|
) -> Result<Option<String>, ParseError> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut out: Option<String> = None;
|
let mut out: Option<String> = None;
|
||||||
@@ -488,7 +482,7 @@ fn read_first_href<R: BufRead>(
|
|||||||
let step = next_step(xml, &mut buf)?;
|
let step = next_step(xml, &mut buf)?;
|
||||||
match step {
|
match step {
|
||||||
Step::StartElement { ns, local, .. } => {
|
Step::StartElement { ns, local, .. } => {
|
||||||
if out.is_none() && is(ns, &local, NsKind::Dav, b"href") {
|
if out.is_none() && is(ns, &local, NsKind::Dav, "href") {
|
||||||
out = Some(read_token(xml)?);
|
out = Some(read_token(xml)?);
|
||||||
} else {
|
} else {
|
||||||
skip_element(xml, &local)?;
|
skip_element(xml, &local)?;
|
||||||
@@ -505,7 +499,7 @@ fn read_first_href<R: BufRead>(
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_element<R: BufRead>(xml: &mut NsReader<R>, target: &[u8]) -> Result<(), ParseError> {
|
fn skip_element<R: BufRead>(xml: &mut NsReader<R>, target: &str) -> Result<(), ParseError> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let mut depth: i32 = 1;
|
let mut depth: i32 = 1;
|
||||||
while depth > 0 {
|
while depth > 0 {
|
||||||
|
|||||||
@@ -435,31 +435,31 @@ fn parse_pox_response(body: &[u8]) -> Result<PoxOutcome, EwsError> {
|
|||||||
let (_, ev) = xml.read_resolved_event_into(&mut buf)?;
|
let (_, ev) = xml.read_resolved_event_into(&mut buf)?;
|
||||||
match ev {
|
match ev {
|
||||||
Event::Start(e) => {
|
Event::Start(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
cur.clear();
|
cur.clear();
|
||||||
if local.eq_ignore_ascii_case(b"Protocol") {
|
if local.eq_ignore_ascii_case("Protocol") {
|
||||||
current_type = None;
|
current_type = None;
|
||||||
}
|
}
|
||||||
if local.eq_ignore_ascii_case(b"Type") {
|
if local.eq_ignore_ascii_case("Type") {
|
||||||
current = Some("type");
|
current = Some("type");
|
||||||
} else if local.eq_ignore_ascii_case(b"EwsUrl") {
|
} else if local.eq_ignore_ascii_case("EwsUrl") {
|
||||||
if matches!(current_type.as_deref(), Some("EXPR") | Some("EXCH")) {
|
if matches!(current_type.as_deref(), Some("EXPR") | Some("EXCH")) {
|
||||||
current = Some("ewsUrl");
|
current = Some("ewsUrl");
|
||||||
} else {
|
} else {
|
||||||
current = None;
|
current = None;
|
||||||
}
|
}
|
||||||
} else if local.eq_ignore_ascii_case(b"Action") {
|
} else if local.eq_ignore_ascii_case("Action") {
|
||||||
current = Some("action");
|
current = Some("action");
|
||||||
} else if local.eq_ignore_ascii_case(b"RedirectAddr") {
|
} else if local.eq_ignore_ascii_case("RedirectAddr") {
|
||||||
current = Some("redirectAddr");
|
current = Some("redirectAddr");
|
||||||
} else if local.eq_ignore_ascii_case(b"RedirectUrl") {
|
} else if local.eq_ignore_ascii_case("RedirectUrl") {
|
||||||
current = Some("redirectUrl");
|
current = Some("redirectUrl");
|
||||||
} else {
|
} else {
|
||||||
current = None;
|
current = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::End(e) => {
|
Event::End(e) => {
|
||||||
let local = e.local_name().as_ref().to_vec();
|
let local = e.local_name().as_ref().to_owned();
|
||||||
if let Some(field) = current.take() {
|
if let Some(field) = current.take() {
|
||||||
let text = std::mem::take(&mut cur);
|
let text = std::mem::take(&mut cur);
|
||||||
match field {
|
match field {
|
||||||
@@ -472,15 +472,15 @@ fn parse_pox_response(body: &[u8]) -> Result<PoxOutcome, EwsError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cur.clear();
|
cur.clear();
|
||||||
if local.eq_ignore_ascii_case(b"Protocol") {
|
if local.eq_ignore_ascii_case("Protocol") {
|
||||||
current_type = None;
|
current_type = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Text(t) => {
|
Event::Text(t) => {
|
||||||
cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default());
|
cur.push_str(&t);
|
||||||
}
|
}
|
||||||
Event::CData(c) => {
|
Event::CData(c) => {
|
||||||
cur.push_str(&String::from_utf8_lossy(c.as_ref()));
|
cur.push_str(&c);
|
||||||
}
|
}
|
||||||
Event::GeneralRef(g) => {
|
Event::GeneralRef(g) => {
|
||||||
if let Some(c) = entity_to_char(&g) {
|
if let Some(c) = entity_to_char(&g) {
|
||||||
|
|||||||
+307
-312
File diff suppressed because it is too large
Load Diff
@@ -655,6 +655,16 @@ mod tests {
|
|||||||
c.authenticate_plain("alice", "p@ss").unwrap();
|
c.authenticate_plain("alice", "p@ss").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticate_plain_accepts_dovecot_post_login_untagged_capability() {
|
||||||
|
let server = b"* CAPABILITY IMAP4rev1 SASL-IR MOVE IMAPSIEVE=sieve://127.0.0.1:4190 QUOTA\r\nA0001 OK [CAPABILITY IMAP4rev1 SASL-IR MOVE IMAPSIEVE=sieve://127.0.0.1:4190 QUOTA] Logged in\r\n";
|
||||||
|
let mut c = client_with(server);
|
||||||
|
c.capabilities.insert("SASL-IR".to_owned());
|
||||||
|
c.authenticate_plain("alice", "p@ss").unwrap();
|
||||||
|
assert!(c.has_capability("MOVE"));
|
||||||
|
assert!(c.has_capability("IMAPSIEVE=sieve://127.0.0.1:4190"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authenticate_plain_continuation_path() {
|
fn authenticate_plain_continuation_path() {
|
||||||
let server = b"+ \r\nA0001 OK auth done\r\n";
|
let server = b"+ \r\nA0001 OK auth done\r\n";
|
||||||
|
|||||||
+66
-26
@@ -190,17 +190,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
}
|
}
|
||||||
Ok(Untagged::StatusLine(line))
|
Ok(Untagged::StatusLine(line))
|
||||||
}
|
}
|
||||||
"CAPABILITY" => {
|
"CAPABILITY" => Ok(Untagged::Capability(self.read_space_separated()?)),
|
||||||
let mut caps = Vec::new();
|
|
||||||
while !self.at_end() {
|
|
||||||
self.skip_ws();
|
|
||||||
if self.at_end() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
caps.push(self.read_atom_string()?);
|
|
||||||
}
|
|
||||||
Ok(Untagged::Capability(caps))
|
|
||||||
}
|
|
||||||
"LIST" | "LSUB" => {
|
"LIST" | "LSUB" => {
|
||||||
self.skip_ws();
|
self.skip_ws();
|
||||||
self.expect(b'(')?;
|
self.expect(b'(')?;
|
||||||
@@ -300,17 +290,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
Ok(Untagged::Flags(flags))
|
Ok(Untagged::Flags(flags))
|
||||||
}
|
}
|
||||||
"NAMESPACE" => self.parse_namespace(),
|
"NAMESPACE" => self.parse_namespace(),
|
||||||
"ENABLED" => {
|
"ENABLED" => Ok(Untagged::Enabled(self.read_space_separated()?)),
|
||||||
let mut exts = Vec::new();
|
|
||||||
while !self.at_end() {
|
|
||||||
self.skip_ws();
|
|
||||||
if self.at_end() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
exts.push(self.read_atom_string()?);
|
|
||||||
}
|
|
||||||
Ok(Untagged::Enabled(exts))
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
if let Ok(num) = upper.parse::<u32>() {
|
if let Ok(num) = upper.parse::<u32>() {
|
||||||
self.skip_ws();
|
self.skip_ws();
|
||||||
@@ -651,6 +631,15 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
.map_err(|e| ImapError::Parse(format!("non-utf8 atom: {e}")))
|
.map_err(|e| ImapError::Parse(format!("non-utf8 atom: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_space_separated(&mut self) -> Result<Vec<String>, ImapError> {
|
||||||
|
let end = self.buf.len().saturating_sub(self.tail_crlf_len());
|
||||||
|
let rest = self.buf.get(self.pos..end).unwrap_or_default();
|
||||||
|
self.pos = self.pos.max(end);
|
||||||
|
std::str::from_utf8(rest)
|
||||||
|
.map(|s| s.split_ascii_whitespace().map(str::to_owned).collect())
|
||||||
|
.map_err(|e| ImapError::Parse(format!("non-utf8 token: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
fn read_seq_set_token(&mut self) -> String {
|
fn read_seq_set_token(&mut self) -> String {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
while let Some(b) = self.peek() {
|
while let Some(b) = self.peek() {
|
||||||
@@ -735,10 +724,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_atom_byte(b: u8) -> bool {
|
fn is_atom_byte(b: u8) -> bool {
|
||||||
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
|
b.is_ascii_graphic() && !matches!(b, b'(' | b')' | b'{' | b'%' | b'"' | b']')
|
||||||
| b'-' | b'.' | b'_' | b'!' | b'#' | b'$' | b'&'
|
|
||||||
| b'\'' | b'+' | b'/' | b';' | b'<' | b'=' | b'>' | b'?'
|
|
||||||
| b'@' | b'\\' | b'^' | b'`' | b'|' | b'~' | b'*')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_seq_set(s: &str) -> Vec<u32> {
|
fn parse_seq_set(s: &str) -> Vec<u32> {
|
||||||
@@ -833,6 +819,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn untagged_capability_accepts_uri_valued_tokens() {
|
||||||
|
let r = parse(
|
||||||
|
b"* CAPABILITY IMAP4rev1 SASL-IR SPECIAL-USE IMAPSIEVE=sieve://127.0.0.1:4190 QUOTA ACL RIGHTS=texk\r\n",
|
||||||
|
);
|
||||||
|
match r {
|
||||||
|
Response::Untagged(Untagged::Capability(caps)) => assert_eq!(
|
||||||
|
caps,
|
||||||
|
vec![
|
||||||
|
"IMAP4rev1",
|
||||||
|
"SASL-IR",
|
||||||
|
"SPECIAL-USE",
|
||||||
|
"IMAPSIEVE=sieve://127.0.0.1:4190",
|
||||||
|
"QUOTA",
|
||||||
|
"ACL",
|
||||||
|
"RIGHTS=texk",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
other => panic!("expected Capability, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn untagged_enabled_accepts_non_atom_tokens() {
|
||||||
|
let r = parse(b"* ENABLED UTF8=ACCEPT X-VENDOR=a:b,c\r\n");
|
||||||
|
match r {
|
||||||
|
Response::Untagged(Untagged::Enabled(exts)) => {
|
||||||
|
assert_eq!(exts, vec!["UTF8=ACCEPT", "X-VENDOR=a:b,c"]);
|
||||||
|
}
|
||||||
|
other => panic!("expected Enabled, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atom_accepts_colon_comma_and_brackets() {
|
||||||
|
let r = parse(b"* LIST (\\HasNoChildren) \"/\" Work:2026,Q1[a}\r\n");
|
||||||
|
match r {
|
||||||
|
Response::Untagged(Untagged::List { name, .. }) => {
|
||||||
|
assert_eq!(name, "Work:2026,Q1[a}");
|
||||||
|
}
|
||||||
|
other => panic!("expected List, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atom_stops_at_atom_specials() {
|
||||||
|
for b in b"(){%\"] \x7f\x00" {
|
||||||
|
assert!(!is_atom_byte(*b), "{b:#04x} must not be an atom byte");
|
||||||
|
}
|
||||||
|
for b in b":,[}=/.-_+" {
|
||||||
|
assert!(is_atom_byte(*b), "{:?} must be an atom byte", *b as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quoted_mailbox_name_with_utf8_bytes_is_decoded_as_utf8() {
|
fn quoted_mailbox_name_with_utf8_bytes_is_decoded_as_utf8() {
|
||||||
let r = parse("* LIST (\\HasNoChildren) \"/\" \"Envoy\u{e9}s\"\r\n".as_bytes());
|
let r = parse("* LIST (\\HasNoChildren) \"/\" \"Envoy\u{e9}s\"\r\n".as_bytes());
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ fn resolve_well_known_roles(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let messages =
|
let messages =
|
||||||
parse_response_messages(&resp.body, b"GetFolderResponseMessage").unwrap_or_default();
|
parse_response_messages(&resp.body, "GetFolderResponseMessage").unwrap_or_default();
|
||||||
let mut map: HashMap<String, &'static str> = HashMap::new();
|
let mut map: HashMap<String, &'static str> = HashMap::new();
|
||||||
for (msg, (_, role)) in messages.into_iter().zip(WELL_KNOWN_ROLES.iter()) {
|
for (msg, (_, role)) in messages.into_iter().zip(WELL_KNOWN_ROLES.iter()) {
|
||||||
if !msg.success {
|
if !msg.success {
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ pub fn get_items(
|
|||||||
for chunk in ids.chunks(batch) {
|
for chunk in ids.chunks(batch) {
|
||||||
let body = get_item_body(shape, chunk, version);
|
let body = get_item_body(shape, chunk, version);
|
||||||
match ctx.client.call(ctx.url, "GetItem", &body) {
|
match ctx.client.call(ctx.url, "GetItem", &body) {
|
||||||
Ok(resp) => match parse_response_messages(&resp.body, b"GetItemResponseMessage") {
|
Ok(resp) => match parse_response_messages(&resp.body, "GetItemResponseMessage") {
|
||||||
Ok(mut msgs) => all.append(&mut msgs),
|
Ok(mut msgs) => all.append(&mut msgs),
|
||||||
Err(e) if is_per_batch_fault(&e) => {
|
Err(e) if is_per_batch_fault(&e) => {
|
||||||
ctx.logger.warn(&format!(
|
ctx.logger.warn(&format!(
|
||||||
@@ -331,7 +331,7 @@ pub fn get_items(
|
|||||||
let body = get_item_body(shape, &chunk, version);
|
let body = get_item_body(shape, &chunk, version);
|
||||||
let n = chunk.len();
|
let n = chunk.len();
|
||||||
let result = match client.call(&url, "GetItem", &body) {
|
let result = match client.call(&url, "GetItem", &body) {
|
||||||
Ok(resp) => parse_response_messages(&resp.body, b"GetItemResponseMessage"),
|
Ok(resp) => parse_response_messages(&resp.body, "GetItemResponseMessage"),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
};
|
};
|
||||||
(n, result)
|
(n, result)
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ service managesieve-login {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_plugins = $mail_plugins imap_sieve
|
||||||
|
}
|
||||||
|
|
||||||
protocol sieve {
|
protocol sieve {
|
||||||
managesieve_max_line_length = 1M
|
managesieve_max_line_length = 1M
|
||||||
managesieve_max_compile_errors = 5
|
managesieve_max_compile_errors = 5
|
||||||
@@ -96,6 +100,7 @@ info_log_path = /dev/stderr
|
|||||||
plugin {
|
plugin {
|
||||||
sieve = file:~/sieve;active=~/.dovecot.sieve
|
sieve = file:~/sieve;active=~/.dovecot.sieve
|
||||||
sieve_max_script_size = 1M
|
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 host = container.get_host()?.to_string();
|
||||||
let imap = Endpoint::new(host.clone(), container.get_host_port_ipv4(IMAP_PORT.tcp())?);
|
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())?);
|
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()
|
let accounts: Vec<Account> = layouts::accounts()
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ impl Stalwart {
|
|||||||
.with_mapped_port(sieve_port, SIEVE_PORT.tcp())
|
.with_mapped_port(sieve_port, SIEVE_PORT.tcp())
|
||||||
.with_startup_timeout(Duration::from_secs(180));
|
.with_startup_timeout(Duration::from_secs(180));
|
||||||
|
|
||||||
let container = request.start()?;
|
let container = request.pull_image()?.start()?;
|
||||||
|
|
||||||
let me = Self {
|
let me = Self {
|
||||||
_container: container,
|
_container: container,
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ use integration::validate::{
|
|||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use vandelay::error::Error;
|
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_imap::{ImapAuth, ImapImportConfig};
|
||||||
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||||
use vandelay::sync::{import_imap, import_managesieve};
|
use vandelay::sync::{import_imap, import_managesieve};
|
||||||
@@ -561,3 +564,28 @@ fn dovecot_non_ascii_mailbox_names_round_trip() {
|
|||||||
);
|
);
|
||||||
cleanup(&archive);
|
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:GetItemResponseMessage>\
|
||||||
</m:ResponseMessages></m:GetItemResponse>"
|
</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_eq!(r.len(), 2);
|
||||||
assert!(r[0].success);
|
assert!(r[0].success);
|
||||||
assert!(r[0].inner_xml.contains("SGVsbG8="));
|
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>\
|
</t:Message></m:Items></m:GetItemResponseMessage>\
|
||||||
</m:ResponseMessages></m:GetItemResponse>"
|
</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 item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
|
||||||
let s = item.mime_content.unwrap();
|
let s = item.mime_content.unwrap();
|
||||||
let bytes = LENIENT.decode(&s).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:Items><t:Message><t:ItemId Id=\"W1\" ChangeKey=\"K\"/></t:Message></m:Items>\
|
||||||
</m:GetItemResponseMessage></m:ResponseMessages></m:GetItemResponse>"
|
</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);
|
assert!(r[0].success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,7 +930,7 @@ fn get_folder_messages_preserve_position_when_one_errors() {
|
|||||||
</m:GetFolderResponseMessage>\
|
</m:GetFolderResponseMessage>\
|
||||||
</m:ResponseMessages></m:GetFolderResponse>"
|
</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_eq!(msgs.len(), 3, "all three messages must be present");
|
||||||
assert!(msgs[0].success);
|
assert!(msgs[0].success);
|
||||||
assert!(!msgs[1].success);
|
assert!(!msgs[1].success);
|
||||||
|
|||||||
+69
-3
@@ -487,9 +487,12 @@ fn write_fetch_message(
|
|||||||
conn.write_raw(b")\r\n")
|
conn.write_raw(b")\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_script_one_folder(uidvalidity: u32, uidnext: u32, uids: &'static [u32]) -> Script {
|
fn serve_one_folder(
|
||||||
Box::new(move |conn: &mut MockConn| -> std::io::Result<()> {
|
conn: &mut MockConn,
|
||||||
auth_preamble(conn, "IMAP4rev2 LITERAL+ AUTH=PLAIN")?;
|
uidvalidity: u32,
|
||||||
|
uidnext: u32,
|
||||||
|
uids: &[u32],
|
||||||
|
) -> std::io::Result<()> {
|
||||||
let (tag, cmd) = conn.read_command()?;
|
let (tag, cmd) = conn.read_command()?;
|
||||||
assert_eq!(cmd, "LIST \"\" \"*\"");
|
assert_eq!(cmd, "LIST \"\" \"*\"");
|
||||||
conn.write_line("* LIST () \"/\" \"INBOX\"")?;
|
conn.write_line("* LIST () \"/\" \"INBOX\"")?;
|
||||||
@@ -507,6 +510,12 @@ fn control_script_one_folder(uidvalidity: u32, uidnext: u32, uids: &'static [u32
|
|||||||
conn.write_line(&format!("{tag} OK SEARCH done"))?;
|
conn.write_line(&format!("{tag} OK SEARCH done"))?;
|
||||||
drain_until_close(conn);
|
drain_until_close(conn);
|
||||||
Ok(())
|
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")?;
|
||||||
|
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()));
|
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]
|
#[test]
|
||||||
fn coordinator_uses_esearch_when_advertised() {
|
fn coordinator_uses_esearch_when_advertised() {
|
||||||
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
let control: Script = Box::new(|conn: &mut MockConn| -> std::io::Result<()> {
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ impl Admin {
|
|||||||
"encryptionAtRest": { "@type": "Disabled" },
|
"encryptionAtRest": { "@type": "Disabled" },
|
||||||
"permissions": { "@type": "Inherit" },
|
"permissions": { "@type": "Inherit" },
|
||||||
"roles": { "@type": role },
|
"roles": { "@type": role },
|
||||||
"locale": "en_US"
|
"locale": "en-US"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let response =
|
let response =
|
||||||
|
|||||||
+9
-1
@@ -1243,6 +1243,10 @@ fn apply_jmap_settings(
|
|||||||
.is_some(),
|
.is_some(),
|
||||||
"x:Jmap/set not applied: {resp}"
|
"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]
|
#[test]
|
||||||
@@ -1255,8 +1259,13 @@ fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
|||||||
let fx = seeder::provision(base_url()).expect("provision");
|
let fx = seeder::provision(base_url()).expect("provision");
|
||||||
let acc = fx.account("test1").expect("test1");
|
let acc = fx.account("test1").expect("test1");
|
||||||
|
|
||||||
|
let blob_size = 8 * 1024 * 1024;
|
||||||
let mut updates = serde_json::Map::new();
|
let mut updates = serde_json::Map::new();
|
||||||
updates.insert("uploadTtl".to_owned(), serde_json::json!(5_000));
|
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 _ttl_guard = JmapSettingsGuard::override_settings(updates);
|
||||||
|
|
||||||
let client = HttpClient::new(basic("test1"), RetryPolicy::new(20), true);
|
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");
|
let limits = session.core_limits().expect("core limits");
|
||||||
client.set_limits(&limits);
|
client.set_limits(&limits);
|
||||||
|
|
||||||
let blob_size = 8 * 1024 * 1024;
|
|
||||||
let mut blob = vec![0u8; blob_size];
|
let mut blob = vec![0u8; blob_size];
|
||||||
let max_uploads = 8u32;
|
let max_uploads = 8u32;
|
||||||
let mut accepted = 0u32;
|
let mut accepted = 0u32;
|
||||||
|
|||||||
Reference in New Issue
Block a user