Fix IMAP: import failed after authenticating to Dovecot when a capability contained :, such as IMAPSIEVE=sieve://... (fixes #43)

This commit is contained in:
Maurus Decimus
2026-09-28 11:38:10 +02:00
parent 5dc92c7329
commit 70fa688d25
17 changed files with 617 additions and 461 deletions
+72 -78
View File
@@ -78,13 +78,13 @@ fn classify_ns(ns: &ResolveResult<'_>) -> NsKind {
match ns {
ResolveResult::Bound(prefix) => {
let p = prefix.as_ref();
if p == NS_DAV.as_bytes() {
if p == NS_DAV {
NsKind::Dav
} else if p == NS_CALDAV.as_bytes() {
} else if p == NS_CALDAV {
NsKind::Caldav
} else if p == NS_CARDDAV.as_bytes() {
} else if p == NS_CARDDAV {
NsKind::Carddav
} else if p == NS_APPLE_ICAL.as_bytes() {
} else if p == NS_APPLE_ICAL {
NsKind::Apple
} else {
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)
}
@@ -102,17 +102,17 @@ fn is(ns: NsKind, local: &[u8], expected_ns: NsKind, target: &[u8]) -> bool {
enum Step {
StartElement {
ns: NsKind,
local: Vec<u8>,
attrs: Vec<(Vec<u8>, Vec<u8>)>,
local: String,
attrs: Vec<(String, String)>,
},
EmptyElement {
ns: NsKind,
local: Vec<u8>,
attrs: Vec<(Vec<u8>, Vec<u8>)>,
local: String,
attrs: Vec<(String, String)>,
},
EndElement {
ns: NsKind,
local: Vec<u8>,
local: String,
},
Text(String),
CData(String),
@@ -125,7 +125,7 @@ fn resolve_entity(g: &BytesRef) -> Option<char> {
if let Ok(Some(c)) = g.resolve_char_ref() {
return Some(c);
}
match g.decode().ok()?.as_ref() {
match g.as_ref() {
"amp" => Some('&'),
"lt" => 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);
Ok(match ev {
Event::Start(e) => {
let local = e.local_name().as_ref().to_vec();
let mut attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for a in e.attributes().flatten() {
attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec()));
}
let local = e.local_name().as_ref().to_owned();
let attrs = e
.attributes()
.flatten()
.map(|a| (a.key.as_ref().to_owned(), a.value.into_owned()))
.collect();
Step::StartElement {
ns: ns_kind,
local,
@@ -153,11 +154,12 @@ fn next_step<R: BufRead>(xml: &mut NsReader<R>, buf: &mut Vec<u8>) -> Result<Ste
}
}
Event::Empty(e) => {
let local = e.local_name().as_ref().to_vec();
let mut attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for a in e.attributes().flatten() {
attrs.push((a.key.as_ref().to_vec(), a.value.as_ref().to_vec()));
}
let local = e.local_name().as_ref().to_owned();
let attrs = e
.attributes()
.flatten()
.map(|a| (a.key.as_ref().to_owned(), a.value.into_owned()))
.collect();
Step::EmptyElement {
ns: ns_kind,
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 {
ns: ns_kind,
local: e.local_name().as_ref().to_vec(),
local: e.local_name().as_ref().to_owned(),
},
Event::Text(t) => {
let s = t.decode().map_err(|e| ParseError::Xml(e.to_string()))?;
Step::Text(s.into_owned())
}
Event::CData(cd) => Step::CData(String::from_utf8_lossy(cd.as_ref()).into_owned()),
Event::Text(t) => Step::Text(t.into_inner().into_owned()),
Event::CData(cd) => Step::CData(cd.into_inner().into_owned()),
Event::GeneralRef(g) => match resolve_entity(&g) {
Some(c) => Step::Entity(c),
None => Step::Other,
@@ -194,7 +193,7 @@ pub fn parse_multistatus<R: BufRead>(
loop {
let step = next_step(&mut xml, &mut buf)?;
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)?
&& seen.insert(r.href.as_str().to_owned())
{
@@ -222,15 +221,15 @@ fn parse_response<R: BufRead>(
let step = next_step(xml, &mut buf)?;
match step {
Step::StartElement { ns, local, .. } => {
if is(ns, &local, NsKind::Dav, b"href") {
if is(ns, &local, NsKind::Dav, "href") {
let text = read_token(xml)?;
if href.is_none() {
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)?;
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)?;
match block_status {
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)?;
}
}
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())),
_ => {}
}
@@ -270,16 +269,16 @@ fn parse_propstat<R: BufRead>(
let step = next_step(xml, &mut buf)?;
match step {
Step::StartElement { ns, local, .. } => {
if is(ns, &local, NsKind::Dav, b"status") {
if is(ns, &local, NsKind::Dav, "status") {
let text = read_token(xml)?;
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)?;
} else {
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())),
_ => {}
}
@@ -299,7 +298,7 @@ fn parse_prop_block<R: BufRead>(
handle_prop_element(xml, props, ns, &local)?;
}
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())),
_ => {}
}
@@ -311,53 +310,53 @@ fn handle_prop_element<R: BufRead>(
xml: &mut NsReader<R>,
props: &mut ResourceProps,
ns: NsKind,
local: &[u8],
local: &str,
) -> Result<(), ParseError> {
if is(ns, local, NsKind::Dav, b"resourcetype") {
if is(ns, local, NsKind::Dav, "resourcetype") {
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)?);
} else if is(ns, local, NsKind::Dav, b"current-user-principal") {
props.current_user_principal = read_first_href(xml, b"current-user-principal")?;
} else if is(ns, local, NsKind::Caldav, b"calendar-home-set") {
props.calendar_home_set = read_first_href(xml, b"calendar-home-set")?;
} else if is(ns, local, NsKind::Carddav, b"addressbook-home-set") {
props.addressbook_home_set = read_first_href(xml, b"addressbook-home-set")?;
} else if is(ns, local, NsKind::Dav, b"getetag") {
} else if is(ns, local, NsKind::Dav, "current-user-principal") {
props.current_user_principal = read_first_href(xml, "current-user-principal")?;
} else if is(ns, local, NsKind::Caldav, "calendar-home-set") {
props.calendar_home_set = read_first_href(xml, "calendar-home-set")?;
} else if is(ns, local, NsKind::Carddav, "addressbook-home-set") {
props.addressbook_home_set = read_first_href(xml, "addressbook-home-set")?;
} else if is(ns, local, NsKind::Dav, "getetag") {
let raw = read_token(xml)?;
if !raw.is_empty() {
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)?);
} else if is(ns, local, NsKind::Dav, b"getlastmodified") {
} else if is(ns, local, NsKind::Dav, "getlastmodified") {
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)?);
} else if is(ns, local, NsKind::Dav, b"getcontentlength") {
} else if is(ns, local, NsKind::Dav, "getcontentlength") {
let raw = read_token(xml)?;
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)?);
} 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)?);
} 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)?);
} else if is(ns, local, NsKind::Apple, b"calendar-order") {
} else if is(ns, local, NsKind::Apple, "calendar-order") {
let raw = read_token(xml)?;
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)?);
} else if is(
ns,
local,
NsKind::Caldav,
b"supported-calendar-component-set",
"supported-calendar-component-set",
) {
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)?);
} 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)?);
} else {
skip_element(xml, local)?;
@@ -380,7 +379,7 @@ fn consume_resourcetype<R: BufRead>(
Step::EmptyElement { ns, local, .. } => {
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 => {
return Err(ParseError::Xml("unexpected EOF in <resourcetype>".into()));
}
@@ -390,12 +389,12 @@ fn consume_resourcetype<R: BufRead>(
Ok(())
}
fn mark_resourcetype(ns: NsKind, local: &[u8], props: &mut ResourceProps) {
if is(ns, local, NsKind::Dav, b"collection") {
fn mark_resourcetype(ns: NsKind, local: &str, props: &mut ResourceProps) {
if is(ns, local, NsKind::Dav, "collection") {
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;
} else if is(ns, local, NsKind::Carddav, b"addressbook") {
} else if is(ns, local, NsKind::Carddav, "addressbook") {
props.is_addressbook = true;
}
}
@@ -412,8 +411,8 @@ fn consume_supported_components<R: BufRead>(
ns,
local,
ref attrs,
} if is(ns, &local, NsKind::Caldav, b"comp") => {
if let Some(name) = attr_lookup(attrs, b"name") {
} if is(ns, &local, NsKind::Caldav, "comp") => {
if let Some(name) = attr_lookup(attrs, "name") {
props.supported_components.push(name);
}
skip_element(xml, &local)?;
@@ -422,8 +421,8 @@ fn consume_supported_components<R: BufRead>(
ns,
local,
ref attrs,
} if is(ns, &local, NsKind::Caldav, b"comp") => {
if let Some(name) = attr_lookup(attrs, b"name") {
} if is(ns, &local, NsKind::Caldav, "comp") => {
if let Some(name) = attr_lookup(attrs, "name") {
props.supported_components.push(name);
}
}
@@ -432,7 +431,7 @@ fn consume_supported_components<R: BufRead>(
ns,
&local,
NsKind::Caldav,
b"supported-calendar-component-set",
"supported-calendar-component-set",
) =>
{
break;
@@ -448,13 +447,8 @@ fn consume_supported_components<R: BufRead>(
Ok(())
}
fn attr_lookup(attrs: &[(Vec<u8>, Vec<u8>)], key: &[u8]) -> Option<String> {
for (k, v) in attrs {
if k == key {
return Some(String::from_utf8_lossy(v).into_owned());
}
}
None
fn attr_lookup(attrs: &[(String, String)], key: &str) -> Option<String> {
attrs.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
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>(
xml: &mut NsReader<R>,
closing_local: &[u8],
closing_local: &str,
) -> Result<Option<String>, ParseError> {
let mut buf = Vec::new();
let mut out: Option<String> = None;
@@ -488,7 +482,7 @@ fn read_first_href<R: BufRead>(
let step = next_step(xml, &mut buf)?;
match step {
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)?);
} else {
skip_element(xml, &local)?;
@@ -505,7 +499,7 @@ fn read_first_href<R: BufRead>(
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 depth: i32 = 1;
while depth > 0 {
+11 -11
View File
@@ -435,31 +435,31 @@ fn parse_pox_response(body: &[u8]) -> Result<PoxOutcome, EwsError> {
let (_, ev) = xml.read_resolved_event_into(&mut buf)?;
match ev {
Event::Start(e) => {
let local = e.local_name().as_ref().to_vec();
let local = e.local_name().as_ref().to_owned();
cur.clear();
if local.eq_ignore_ascii_case(b"Protocol") {
if local.eq_ignore_ascii_case("Protocol") {
current_type = None;
}
if local.eq_ignore_ascii_case(b"Type") {
if local.eq_ignore_ascii_case("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")) {
current = Some("ewsUrl");
} else {
current = None;
}
} else if local.eq_ignore_ascii_case(b"Action") {
} else if local.eq_ignore_ascii_case("Action") {
current = Some("action");
} else if local.eq_ignore_ascii_case(b"RedirectAddr") {
} else if local.eq_ignore_ascii_case("RedirectAddr") {
current = Some("redirectAddr");
} else if local.eq_ignore_ascii_case(b"RedirectUrl") {
} else if local.eq_ignore_ascii_case("RedirectUrl") {
current = Some("redirectUrl");
} else {
current = None;
}
}
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() {
let text = std::mem::take(&mut cur);
match field {
@@ -472,15 +472,15 @@ fn parse_pox_response(body: &[u8]) -> Result<PoxOutcome, EwsError> {
}
}
cur.clear();
if local.eq_ignore_ascii_case(b"Protocol") {
if local.eq_ignore_ascii_case("Protocol") {
current_type = None;
}
}
Event::Text(t) => {
cur.push_str(&t.decode().map(|c| c.into_owned()).unwrap_or_default());
cur.push_str(&t);
}
Event::CData(c) => {
cur.push_str(&String::from_utf8_lossy(c.as_ref()));
cur.push_str(&c);
}
Event::GeneralRef(g) => {
if let Some(c) = entity_to_char(&g) {
+307 -312
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -655,6 +655,16 @@ mod tests {
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]
fn authenticate_plain_continuation_path() {
let server = b"+ \r\nA0001 OK auth done\r\n";
+66 -26
View File
@@ -190,17 +190,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
}
Ok(Untagged::StatusLine(line))
}
"CAPABILITY" => {
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))
}
"CAPABILITY" => Ok(Untagged::Capability(self.read_space_separated()?)),
"LIST" | "LSUB" => {
self.skip_ws();
self.expect(b'(')?;
@@ -300,17 +290,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
Ok(Untagged::Flags(flags))
}
"NAMESPACE" => self.parse_namespace(),
"ENABLED" => {
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))
}
"ENABLED" => Ok(Untagged::Enabled(self.read_space_separated()?)),
_ => {
if let Ok(num) = upper.parse::<u32>() {
self.skip_ws();
@@ -651,6 +631,15 @@ impl<'r, R: BufRead> Parser<'r, R> {
.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 {
let start = self.pos;
while let Some(b) = self.peek() {
@@ -735,10 +724,7 @@ impl<'r, R: BufRead> Parser<'r, R> {
}
fn is_atom_byte(b: u8) -> bool {
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
| b'-' | b'.' | b'_' | b'!' | b'#' | b'$' | b'&'
| b'\'' | b'+' | b'/' | b';' | b'<' | b'=' | b'>' | b'?'
| b'@' | b'\\' | b'^' | b'`' | b'|' | b'~' | b'*')
b.is_ascii_graphic() && !matches!(b, b'(' | b')' | b'{' | b'%' | b'"' | b']')
}
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]
fn quoted_mailbox_name_with_utf8_bytes_is_decoded_as_utf8() {
let r = parse("* LIST (\\HasNoChildren) \"/\" \"Envoy\u{e9}s\"\r\n".as_bytes());
+1 -1
View File
@@ -131,7 +131,7 @@ fn resolve_well_known_roles(
}
};
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();
for (msg, (_, role)) in messages.into_iter().zip(WELL_KNOWN_ROLES.iter()) {
if !msg.success {
+2 -2
View File
@@ -294,7 +294,7 @@ pub fn get_items(
for chunk in ids.chunks(batch) {
let body = get_item_body(shape, chunk, version);
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),
Err(e) if is_per_batch_fault(&e) => {
ctx.logger.warn(&format!(
@@ -331,7 +331,7 @@ pub fn get_items(
let body = get_item_body(shape, &chunk, version);
let n = chunk.len();
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),
};
(n, result)