Handle tag desynchronization (#284)

This commit is contained in:
Clément DOUIN 2024-03-31 09:34:21 +02:00 committed by GitHub
parent 345bd64487
commit af5ad735bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 2 deletions

View file

@ -8,6 +8,8 @@ use std::ops::{Deref, DerefMut};
use std::str; use std::str;
use std::sync::mpsc; use std::sync::mpsc;
use crate::error::TagMismatch;
use super::authenticator::Authenticator; use super::authenticator::Authenticator;
use super::error::{Bad, Bye, Error, No, ParseError, Result, ValidateError}; use super::error::{Bad, Bye, Error, No, ParseError, Result, ValidateError};
use super::extensions; use super::extensions;
@ -171,6 +173,17 @@ pub struct Connection<T: Read + Write> {
pub greeting_read: bool, pub greeting_read: bool,
} }
impl<T: Read + Write> Connection<T> {
/// Manually increment the current tag.
///
/// This function can be manually executed by callers when the
/// previous tag was not reused, for example when a timeout did
/// not write anything on the stream.
pub fn skip_tag(&mut self) {
self.tag += 1;
}
}
/// A builder for the append command /// A builder for the append command
#[must_use] #[must_use]
pub struct AppendCmd<'a, T: Read + Write> { pub struct AppendCmd<'a, T: Read + Write> {
@ -1495,7 +1508,12 @@ impl<T: Read + Write> Connection<T> {
fn run_command(&mut self, untagged_command: &str) -> Result<()> { fn run_command(&mut self, untagged_command: &str) -> Result<()> {
let command = self.create_command(untagged_command); let command = self.create_command(untagged_command);
self.write_line(command.into_bytes().as_slice()) let result = self.write_line(command.into_bytes().as_slice());
if result.is_err() {
// rollback tag increased in create_command()
self.tag -= 1;
}
result
} }
fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result<Vec<u8>> { fn run_command_and_read_response(&mut self, untagged_command: &str) -> Result<Vec<u8>> {
@ -1547,7 +1565,17 @@ impl<T: Read + Write> Connection<T> {
.. ..
}, },
)) => { )) => {
assert_eq!(tag.as_bytes(), match_tag.as_bytes()); // check if tag matches
if tag.as_bytes() != match_tag.as_bytes() {
let expect = self.tag;
let actual = tag
.0
.trim_start_matches(TAG_PREFIX)
.parse::<u32>()
.map_err(|_| tag.as_bytes().to_vec());
break Err(Error::TagMismatch(TagMismatch { expect, actual }));
}
Some(match status { Some(match status {
Status::Bad | Status::No | Status::Bye => Err(( Status::Bad | Status::No | Status::Bye => Err((
status, status,

View file

@ -105,6 +105,10 @@ pub enum Error {
/// In response to a STATUS command, the server sent OK without actually sending any STATUS /// In response to a STATUS command, the server sent OK without actually sending any STATUS
/// responses first. /// responses first.
MissingStatusResponse, MissingStatusResponse,
/// The server responded with a different command tag than the one we just sent.
///
/// A new session must generally be established to recover from this.
TagMismatch(TagMismatch),
/// StartTls is not available on the server /// StartTls is not available on the server
StartTlsNotAvailable, StartTlsNotAvailable,
/// Returns when Tls is not configured /// Returns when Tls is not configured
@ -175,6 +179,7 @@ impl fmt::Display for Error {
Error::Append => f.write_str("Could not append mail to mailbox"), Error::Append => f.write_str("Could not append mail to mailbox"),
Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r), Error::Unexpected(ref r) => write!(f, "Unexpected Response: {:?}", r),
Error::MissingStatusResponse => write!(f, "Missing STATUS Response"), Error::MissingStatusResponse => write!(f, "Missing STATUS Response"),
Error::TagMismatch(ref data) => write!(f, "Mismatched Tag: {:?}", data),
Error::StartTlsNotAvailable => write!(f, "StartTls is not available on the server"), Error::StartTlsNotAvailable => write!(f, "StartTls is not available on the server"),
Error::TlsNotConfigured => { Error::TlsNotConfigured => {
write!(f, "TLS was requested, but no TLS features are enabled") write!(f, "TLS was requested, but no TLS features are enabled")
@ -203,6 +208,7 @@ impl StdError for Error {
Error::Append => "Could not append mail to mailbox", Error::Append => "Could not append mail to mailbox",
Error::Unexpected(_) => "Unexpected Response", Error::Unexpected(_) => "Unexpected Response",
Error::MissingStatusResponse => "Missing STATUS Response", Error::MissingStatusResponse => "Missing STATUS Response",
Error::TagMismatch(ref e) => e.description(),
Error::StartTlsNotAvailable => "StartTls is not available on the server", Error::StartTlsNotAvailable => "StartTls is not available on the server",
Error::TlsNotConfigured => "TLS was requested, but no TLS features are enabled", Error::TlsNotConfigured => "TLS was requested, but no TLS features are enabled",
} }
@ -218,6 +224,7 @@ impl StdError for Error {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
Error::TlsHandshake(ref e) => Some(e), Error::TlsHandshake(ref e) => Some(e),
Error::Parse(ParseError::DataNotUtf8(_, ref e)) => Some(e), Error::Parse(ParseError::DataNotUtf8(_, ref e)) => Some(e),
Error::TagMismatch(ref e) => Some(e),
_ => None, _ => None,
} }
} }
@ -296,6 +303,28 @@ impl StdError for ValidateError {
} }
} }
/// The server responded with a different command tag than last one we sent.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TagMismatch {
/// Expected tag number
pub(crate) expect: u32,
/// Actual tag number, 0 if parse failed
pub(crate) actual: std::result::Result<u32, Vec<u8>>,
}
impl fmt::Display for TagMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Expected tag number is {}, actual {:?}",
self.expect, self.actual
)
}
}
impl StdError for TagMismatch {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;