mirror of https://git.meli.delivery/meli/meli
melib/imap: add imap_connection test WIP
parent
073d43b9b8
commit
80e6d3fd06
@ -0,0 +1,9 @@
|
||||
Cargo.lock
|
||||
target
|
||||
.idea
|
||||
stunnel
|
||||
stunnel-5.70
|
||||
stunnel.conf
|
||||
# Coverage
|
||||
**/*.profraw
|
||||
coveralls.lcov
|
@ -0,0 +1,18 @@
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_BIN ?= cargo
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-client clean-server clean-support
|
||||
|
||||
.PHONY: clean-client
|
||||
clean-client:
|
||||
@cd tokio-client && ${CARGO_BIN} clean
|
||||
|
||||
.PHONY: clean-server
|
||||
clean-server:
|
||||
@cd tokio-server && ${CARGO_BIN} clean
|
||||
|
||||
.PHONY: clean-support
|
||||
clean-support:
|
||||
@cd tokio-support && ${CARGO_BIN} clean
|
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
|
||||
tar -xJvf stunnel-5.70.tar.xz
|
||||
cd stunnel-5.70
|
||||
./configure LDFLAGS='--static' --with-pic --disable-systemd --disable-fips --disable-ipv6 --disable-largefile --disable-libtool-lock --enable-static --enable-static=yes --enable-shared=no --disable-dependency-tracking
|
||||
make
|
||||
cd ..
|
||||
cp -i stunnel-5.70/src/stunnel .
|
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
|
||||
openssl req -x509 -out localhost.crt -keyout localhost.key \
|
||||
-newkey rsa:2048 -nodes -sha256 \
|
||||
-subj '/CN=localhost' -extensions EXT -config <( \
|
||||
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
|
Binary file not shown.
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "tokio-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Damian Poddebniak <poddebniak@mailbox.org>"]
|
||||
repository = "https://github.com/duesee/imap-codec"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
#imap-codec = { version = "0.11.0", features = ["ext_sasl_ir", "ext_literal", "ext_idle", "ext_enable"] }
|
||||
imap-codec = { path = "../../../../../../imap-codec/imap-codec", features = ["ext_sasl_ir", "ext_literal", "ext_idle", "ext_enable"] }
|
||||
tokio-support = { path = "../tokio-support" }
|
||||
|
||||
[workspace]
|
@ -0,0 +1,129 @@
|
||||
use anyhow::{Context, Error};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use imap_codec::imap_types::{
|
||||
command::{Command, CommandBody},
|
||||
core::Tag,
|
||||
response::{Response, Status},
|
||||
};
|
||||
use tokio::{self, net::TcpStream};
|
||||
use tokio_support::client::{Event, ImapClientCodec};
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
// Poor human's terminal color support.
|
||||
const BLUE: &str = "\x1b[34m";
|
||||
const RED: &str = "\x1b[31m";
|
||||
const RESET: &str = "\x1b[0m";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let addr = std::env::args()
|
||||
.nth(1)
|
||||
.context("USAGE: tokio-client <host>:<port>")?;
|
||||
|
||||
let mut framed = {
|
||||
let stream = TcpStream::connect(&addr)
|
||||
.await
|
||||
.context(format!("Could not connect to `{addr}`"))?;
|
||||
// This is for demonstration purposes only, and we probably want a bigger number.
|
||||
let max_literal_size = 1024;
|
||||
|
||||
ImapClientCodec::new(max_literal_size).framed(stream)
|
||||
};
|
||||
|
||||
// First, we read the server greeting.
|
||||
match framed
|
||||
.next()
|
||||
.await
|
||||
// We get an `Option<Result<...>>` here that denotes ...
|
||||
// 1) if we got something from the server, and
|
||||
// 2) if it was valid.
|
||||
.context("Connection closed unexpectedly")?
|
||||
.context("Failed to obtain next message")?
|
||||
{
|
||||
Event::Greeting(greeting) => {
|
||||
println!("S: {BLUE}{greeting:#?}{RESET}");
|
||||
}
|
||||
Event::Response(response) => {
|
||||
return Err(Error::msg(format!("Expected greeting, got `{response:?}`")));
|
||||
}
|
||||
};
|
||||
|
||||
// Then, we send a login command to the server ...
|
||||
let tag_login = Tag::unvalidated("A1");
|
||||
let cmd = Command {
|
||||
tag: tag_login.clone(),
|
||||
body: CommandBody::login("alice", "password").context("Could not create command")?,
|
||||
};
|
||||
framed.send(&cmd).await.context("Could not send command")?;
|
||||
println!("C: {RED}{cmd:#?}{RESET}");
|
||||
|
||||
// ... and process the response(s). We must read zero or many data responses before we can
|
||||
// finally examine the status response that tells us whether the login succeeded.
|
||||
loop {
|
||||
let frame = framed
|
||||
.next()
|
||||
.await
|
||||
.context("Connection closed unexpectedly")?
|
||||
.context("Failed to obtain next message")?;
|
||||
println!("S: {BLUE}{frame:#?}{RESET}");
|
||||
|
||||
match frame {
|
||||
Event::Greeting(greeting) => {
|
||||
return Err(Error::msg(format!("Expected response, got `{greeting:?}`")));
|
||||
}
|
||||
Event::Response(response) => match response {
|
||||
Response::Status(ref status) if status.tag() == Some(&tag_login) => {
|
||||
if matches!(status, Status::Ok { .. }) {
|
||||
println!("[!] got login done (successful)");
|
||||
} else {
|
||||
println!("[!] got login done (failed)");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
println!("[!] unexpected response");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let tag_logout = Tag::unvalidated("A2");
|
||||
let cmd = Command {
|
||||
tag: tag_logout.clone(),
|
||||
body: CommandBody::Logout,
|
||||
};
|
||||
framed.send(&cmd).await.context("Could not send command")?;
|
||||
println!("C: {RED}{cmd:#?}{RESET}");
|
||||
|
||||
loop {
|
||||
let frame = framed
|
||||
.next()
|
||||
.await
|
||||
.context("Connection closed unexpectedly")?
|
||||
.context("Failed to obtain next message")?;
|
||||
println!("S: {BLUE}{frame:#?}{RESET}");
|
||||
|
||||
match frame {
|
||||
Event::Greeting(greeting) => {
|
||||
return Err(Error::msg(format!("Expected response, got `{greeting:?}`")));
|
||||
}
|
||||
Event::Response(response) => match response {
|
||||
Response::Status(Status::Bye { .. }) => {
|
||||
println!("[!] got bye");
|
||||
}
|
||||
Response::Status(Status::Ok {
|
||||
tag: Some(ref tag), ..
|
||||
}) if *tag == tag_logout => {
|
||||
println!("[!] got logout done");
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
println!("[!] unexpected response");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "tokio-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Damian Poddebniak <poddebniak@mailbox.org>"]
|
||||
repository = "https://github.com/duesee/imap-codec"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argon2 = "0.5.0"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
#imap-codec = { version = "0.11.0" }
|
||||
imap-codec = { path = "../../../../../../imap-codec/imap-codec" }
|
||||
tokio-support = { path = "../tokio-support" }
|
||||
|
||||
[workspace]
|
@ -0,0 +1,166 @@
|
||||
use anyhow::{Context, Error};
|
||||
use argon2::Argon2;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use imap_codec::imap_types::{
|
||||
command::CommandBody,
|
||||
core::{NonEmptyVec, Text},
|
||||
response::{Capability, Continue, Data, Greeting, Response, Status},
|
||||
};
|
||||
use tokio::{self, net::TcpListener};
|
||||
use tokio_support::server::{Action, Event, ImapServerCodec};
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
// Poor human's terminal color support.
|
||||
const BLUE: &str = "\x1b[34m";
|
||||
const RED: &str = "\x1b[31m";
|
||||
const RESET: &str = "\x1b[0m";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let addr = std::env::args()
|
||||
.nth(1)
|
||||
.context("USAGE: tokio-server <host>:<port>")?;
|
||||
|
||||
let mut framed = {
|
||||
let stream = {
|
||||
// Bind listener ...
|
||||
let listener = TcpListener::bind(&addr)
|
||||
.await
|
||||
.context(format!("Could not bind to `{addr}`"))?;
|
||||
|
||||
// ... and accept a single connection.
|
||||
let (stream, _) = listener
|
||||
.accept()
|
||||
.await
|
||||
.context("Could not accept connection")?;
|
||||
|
||||
stream
|
||||
};
|
||||
|
||||
// Accept 2 MiB literals.
|
||||
let mib2 = 2 * 1024 * 1024;
|
||||
ImapServerCodec::new(mib2).framed(stream)
|
||||
};
|
||||
|
||||
// Send a positive greeting ...
|
||||
let greeting = Greeting::ok(None, "Hello, World!").context("Could not create greeting")?;
|
||||
framed
|
||||
.send(&greeting)
|
||||
.await
|
||||
.context("Could not send greeting")?;
|
||||
println!("S: {BLUE}{greeting:#?}{RESET}");
|
||||
|
||||
// ... and process the following commands in a loop.
|
||||
loop {
|
||||
match framed
|
||||
.next()
|
||||
.await
|
||||
.context("Connection closed unexpectedly")?
|
||||
.context("Failed to obtain next message")?
|
||||
{
|
||||
Event::Command(cmd) => {
|
||||
println!("C: {RED}{cmd:#?}{RESET}");
|
||||
|
||||
match (cmd.tag, cmd.body) {
|
||||
(tag, CommandBody::Capability) => {
|
||||
let rsp = Response::Data(Data::Capability(NonEmptyVec::from(
|
||||
Capability::Imap4Rev1,
|
||||
)));
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
|
||||
let rsp = Response::Status(
|
||||
Status::ok(Some(tag), None, "CAPABILITY done")
|
||||
.context("Could not create `Status`")?,
|
||||
);
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
}
|
||||
(tag, CommandBody::Login { username, password }) => {
|
||||
let login_okay = {
|
||||
let username_okay = username.as_ref() == b"alice";
|
||||
let password_okay = {
|
||||
// Salt should be unique per password.
|
||||
let salt = b"hf63l9nx43gf95ks";
|
||||
let password = password.declassify().as_ref();
|
||||
|
||||
let mut output = [0u8; 32];
|
||||
Argon2::default()
|
||||
.hash_password_into(password, salt, &mut output)
|
||||
.map_err(|error| Error::msg(error.to_string()))
|
||||
.context("Failed to hash password.")?;
|
||||
|
||||
output
|
||||
== [
|
||||
227, 130, 151, 49, 100, 203, 239, 68, 119, 207, 247, 237,
|
||||
214, 42, 85, 208, 198, 107, 116, 35, 64, 122, 143, 68, 236,
|
||||
228, 130, 250, 31, 221, 217, 77,
|
||||
]
|
||||
};
|
||||
|
||||
username_okay && password_okay
|
||||
};
|
||||
|
||||
let rsp = if login_okay {
|
||||
Response::Status(Status::Ok {
|
||||
tag: Some(tag),
|
||||
code: None,
|
||||
text: Text::unvalidated("LOGIN succeeded"),
|
||||
})
|
||||
} else {
|
||||
Response::Status(Status::Ok {
|
||||
tag: Some(tag),
|
||||
code: None,
|
||||
text: Text::unvalidated("LOGIN failed"),
|
||||
})
|
||||
};
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
}
|
||||
(tag, CommandBody::Logout) => {
|
||||
let rsp = Response::Status(
|
||||
Status::bye(None, "...").expect("Could not create `Status`"),
|
||||
);
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
|
||||
let rsp = Response::Status(
|
||||
Status::ok(Some(tag), None, "LOGOUT done")
|
||||
.expect("Could not create `Status`"),
|
||||
);
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
(tag, body) => {
|
||||
let text = format!("{} not supported", body.name());
|
||||
let rsp = Response::Status(
|
||||
Status::no(Some(tag), None, text)
|
||||
.context("Could not create `Status`")?,
|
||||
);
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::ActionRequired(Action::SendLiteralAck(_)) => {
|
||||
println!("[!] Send continuation request.");
|
||||
let rsp = Response::Continue(
|
||||
Continue::basic(None, "...").context("Could not create `Continue`")?,
|
||||
);
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
}
|
||||
Event::ActionRequired(Action::SendLiteralReject(_)) => {
|
||||
println!("[!] Send literal reject.");
|
||||
let rsp = Response::Status(
|
||||
Status::bad(None, None, "literal too large.")
|
||||
.context("Could not create `Status`")?,
|
||||
);
|
||||
framed.send(&rsp).await.context("Could not send response")?;
|
||||
println!("S: {BLUE}{rsp:#?}{RESET}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "tokio-support"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Damian Poddebniak <poddebniak@mailbox.org>"]
|
||||
repository = "https://github.com/duesee/imap-codec"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.4.0"
|
||||
bounded-static = "0.5.0"
|
||||
thiserror = "1.0.29"
|
||||
tokio-util = { version = "0.7.8", features = ["codec"] }
|
||||
|
||||
#imap-codec = { version = "0.11.0", features = ["bounded-static"] }
|
||||
imap-codec = { path = "../../../../../../imap-codec/imap-codec", features = ["bounded-static"] }
|
||||
|
||||
[workspace]
|
@ -0,0 +1,343 @@
|
||||
use std::io::{Error as IoError, Write};
|
||||
|
||||
use bounded_static::IntoBoundedStatic;
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use imap_codec::{
|
||||
codec::{Decode, DecodeError, Encode},
|
||||
imap_types::{
|
||||
command::Command,
|
||||
response::{Greeting, Response},
|
||||
state::State as ImapState,
|
||||
},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use super::{find_crlf_inclusive, FramingError, FramingState};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ImapClientCodec {
|
||||
state: FramingState,
|
||||
imap_state: ImapState<'static>,
|
||||
max_literal_length: u32,
|
||||
}
|
||||
|
||||
impl ImapClientCodec {
|
||||
pub fn new(max_literal_length: u32) -> Self {
|
||||
Self {
|
||||
state: FramingState::ReadLine { to_consume_acc: 0 },
|
||||
imap_state: ImapState::Greeting,
|
||||
max_literal_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ImapClientCodecError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Framing(#[from] FramingError),
|
||||
#[error("Parsing failed")]
|
||||
ParsingFailed(BytesMut),
|
||||
}
|
||||
|
||||
impl PartialEq for ImapClientCodecError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Io(error1), Self::Io(error2)) => error1.kind() == error2.kind(),
|
||||
(Self::Framing(kind2), Self::Framing(kind1)) => kind1 == kind2,
|
||||
(Self::ParsingFailed(x), Self::ParsingFailed(y)) => x == y,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
Greeting(Greeting<'static>),
|
||||
Response(Response<'static>),
|
||||
}
|
||||
|
||||
impl Decoder for ImapClientCodec {
|
||||
type Item = Event;
|
||||
type Error = ImapClientCodecError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
loop {
|
||||
match self.state {
|
||||
FramingState::ReadLine {
|
||||
ref mut to_consume_acc,
|
||||
} => {
|
||||
match find_crlf_inclusive(*to_consume_acc, src) {
|
||||
Some(line) => match line {
|
||||
// After skipping `to_consume_acc` bytes, we need `to_consume` more
|
||||
// bytes to form a full line (including the `\r\n`).
|
||||
Ok(to_consume) => {
|
||||
*to_consume_acc += to_consume;
|
||||
let line = &src[..*to_consume_acc];
|
||||
|
||||
// TODO: Choose the required parser.
|
||||
let parser = match self.imap_state {
|
||||
ImapState::Greeting => |input| {
|
||||
Greeting::decode(input).map(|(rem, grt)| {
|
||||
(rem, Event::Greeting(grt.into_static()))
|
||||
})
|
||||
},
|
||||
_ => |input| {
|
||||
Response::decode(input).map(|(rem, rsp)| {
|
||||
(rem, Event::Response(rsp.into_static()))
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
match parser(line) {
|
||||
// We got a complete message.
|
||||
Ok((rem, outcome)) => {
|
||||
assert!(rem.is_empty());
|
||||
|
||||
src.advance(*to_consume_acc);
|
||||
self.state = FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
if self.imap_state == ImapState::Greeting {
|
||||
self.imap_state = ImapState::NotAuthenticated;
|
||||
}
|
||||
|
||||
return Ok(Some(outcome));
|
||||
}
|
||||
Err(error) => match error {
|
||||
// We supposedly need more data ...
|
||||
//
|
||||
// This should not happen because a line that doesn't end
|
||||
// with a literal is always "complete" in IMAP.
|
||||
DecodeError::Incomplete => {
|
||||
unreachable!();
|
||||
}
|
||||
// We found a literal.
|
||||
DecodeError::LiteralFound { length, .. } => {
|
||||
if length <= self.max_literal_length {
|
||||
src.reserve(length as usize);
|
||||
|
||||
self.state = FramingState::ReadLiteral {
|
||||
to_consume_acc: *to_consume_acc,
|
||||
length,
|
||||
};
|
||||
|
||||
return Ok(None);
|
||||
} else {
|
||||
src.advance(*to_consume_acc);
|
||||
|
||||
self.state =
|
||||
FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
return Err(ImapClientCodecError::Framing(
|
||||
FramingError::LiteralTooLarge {
|
||||
max_literal_length: self.max_literal_length,
|
||||
length,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
DecodeError::Failed => {
|
||||
let consumed = src.split_to(*to_consume_acc);
|
||||
self.state =
|
||||
FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
return Err(ImapClientCodecError::ParsingFailed(
|
||||
consumed,
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// After skipping `to_consume_acc` bytes, we need `to_consume` more
|
||||
// bytes to form a full line (including the `\n`).
|
||||
//
|
||||
// Note: This line is missing the `\r\n` and should be discarded.
|
||||
Err(to_discard) => {
|
||||
*to_consume_acc += to_discard;
|
||||
src.advance(*to_consume_acc);
|
||||
|
||||
self.state = FramingState::ReadLine { to_consume_acc: 0 };
|
||||
return Err(ImapClientCodecError::Framing(FramingError::NotCrLf));
|
||||
}
|
||||
},
|
||||
// More data needed.
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
FramingState::ReadLiteral {
|
||||
to_consume_acc,
|
||||
length,
|
||||
} => {
|
||||
if to_consume_acc + length as usize <= src.len() {
|
||||
self.state = FramingState::ReadLine {
|
||||
to_consume_acc: to_consume_acc + length as usize,
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encoder<&Command<'a>> for ImapClientCodec {
|
||||
type Error = IoError;
|
||||
|
||||
fn encode(&mut self, item: &Command, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
//dst.reserve(item.len());
|
||||
let mut writer = dst.writer();
|
||||
// TODO(225): Don't use `dump` here.
|
||||
let data = item.encode().dump();
|
||||
writer.write_all(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use imap_codec::imap_types::{
|
||||
core::{Literal, NString},
|
||||
fetch::{MessageDataItem, Section},
|
||||
response::{Data, GreetingKind},
|
||||
};
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_decoder_line() {
|
||||
let tests = [
|
||||
(b"".as_ref(), Ok(None)),
|
||||
(b"* ", Ok(None)),
|
||||
(b"OK ...\r", Ok(None)),
|
||||
(
|
||||
b"\n",
|
||||
Ok(Some(Event::Greeting(
|
||||
Greeting::new(GreetingKind::Ok, None, "...").unwrap(),
|
||||
))),
|
||||
),
|
||||
(b"", Ok(None)),
|
||||
(b"xxxx", Ok(None)),
|
||||
(
|
||||
b"\r\n",
|
||||
Err(ImapClientCodecError::ParsingFailed(BytesMut::from(
|
||||
b"xxxx\r\n".as_ref(),
|
||||
))),
|
||||
),
|
||||
];
|
||||
|
||||
let mut src = BytesMut::new();
|
||||
let mut codec = ImapClientCodec::new(1024);
|
||||
|
||||
for (test, expected) in tests {
|
||||
src.extend_from_slice(test);
|
||||
let got = codec.decode(&mut src);
|
||||
|
||||
assert_eq!(expected, got);
|
||||
|
||||
dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoder_literal() {
|
||||
let tests = [
|
||||
(
|
||||
b"* OK ...\r\n".as_ref(),
|
||||
Ok(Some(Event::Greeting(
|
||||
Greeting::new(GreetingKind::Ok, None, "...").unwrap(),
|
||||
))),
|
||||
),
|
||||
(b"* 12 FETCH (BODY[HEADER] {3}", Ok(None)),
|
||||
(b"\r", Ok(None)),
|
||||
(b"\n", Ok(None)),
|
||||
(b"a", Ok(None)),
|
||||
(b"bc)", Ok(None)),
|
||||
(b"\r", Ok(None)),
|
||||
(
|
||||
b"\n",
|
||||
Ok(Some(Event::Response(Response::Data(
|
||||
Data::fetch(
|
||||
12,
|
||||
vec![MessageDataItem::BodyExt {
|
||||
section: Some(Section::Header(None)),
|
||||
origin: None,
|
||||
data: NString(Some(Literal::try_from("abc").unwrap().into())),
|
||||
}],
|
||||
)
|
||||
.unwrap(),
|
||||
)))),
|
||||
),
|
||||
];
|
||||
|
||||
let mut src = BytesMut::new();
|
||||
let mut codec = ImapClientCodec::new(1024);
|
||||
|
||||
for (test, expected) in tests {
|
||||
src.extend_from_slice(test);
|
||||
let got = codec.decode(&mut src);
|
||||
|
||||
dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
|
||||
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoder_error() {
|
||||
let tests = [
|
||||
// We need to process the greeting first.
|
||||
(
|
||||
b"* OK ...\r\n".as_ref(),
|
||||
Ok(Some(Event::Greeting(
|
||||
Greeting::new(GreetingKind::Ok, None, "...").unwrap(),
|
||||
))),
|
||||
),
|
||||
(
|
||||
b"xxx\r\n".as_ref(),
|
||||
Err(ImapClientCodecError::ParsingFailed(BytesMut::from(
|
||||
b"xxx\r\n".as_ref(),
|
||||
))),
|
||||
),
|
||||
(
|
||||
b"* search 1\n",
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
Err(ImapClientCodecError::Framing(FramingError::NotCrLf)),
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
Ok(Some(Event::Response(Response::Data(Data::Search(vec![
|
||||
NonZeroU32::try_from(1).unwrap(),
|
||||
]))))),
|
||||
),
|
||||
(
|
||||
b"* 1 fetch (BODY[] {17}\r\naaaaaaaaaaaaaaaa)\r\n",
|
||||
Err(ImapClientCodecError::Framing(
|
||||
FramingError::LiteralTooLarge {
|
||||
max_literal_length: 16,
|
||||
length: 17,
|
||||
},
|
||||
)),
|
||||
),
|
||||
];
|
||||
|
||||
let mut src = BytesMut::new();
|
||||
let mut codec = ImapClientCodec::new(16);
|
||||
|
||||
for (test, expected) in tests {
|
||||
src.extend_from_slice(test);
|
||||
let got = codec.decode(&mut src);
|
||||
|
||||
dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
|
||||
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
//! Support for tokio and (tokio_util::codec).
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
|
||||
/// All interactions transmitted by client and server are in the form of
|
||||
/// lines, that is, strings that end with a CRLF.
|
||||
///
|
||||
/// The protocol receiver of an IMAP4rev1 client or server is either ...
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum FramingState {
|
||||
/// ... reading a line, or ...
|
||||
ReadLine { to_consume_acc: usize },
|
||||
/// ... is reading a sequence of octets
|
||||
/// with a known count followed by a line.
|
||||
ReadLiteral { to_consume_acc: usize, length: u32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum FramingError {
|
||||
#[error("Expected `\\r\\n`, got `\\n`")]
|
||||
NotCrLf,
|
||||
#[error("Could not find a line searching a maximum of {max_line_length} bytes")]
|
||||
LineTooLarge { max_line_length: u32 },
|
||||
#[error("Could not find a message while searching a maximum of {max_message_length} bytes")]
|
||||
MessageTooLarge { max_message_length: u32 },
|
||||
#[error("Expected a maximum literal length of {max_literal_length} bytes, got {length} bytes")]
|
||||
LiteralTooLarge {
|
||||
max_literal_length: u32,
|
||||
length: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Skip the first `skip` bytes of `buf` and count how many more bytes are needed to cover the next `\r\n`.
|
||||
///
|
||||
/// This function returns `Ok(None)` when no line was found, `Ok(Some(length))` with
|
||||
/// `buf[..skip + length]` being the first line (including `\r\n`), or `Err(length)` with
|
||||
/// `buf[..skip + length]` being the first line (including `\n`) with a missing `\r`.
|
||||
fn find_crlf_inclusive(skip: usize, buf: &[u8]) -> Option<Result<usize, usize>> {
|
||||
#[allow(clippy::manual_map)]
|
||||
match buf.iter().skip(skip).position(|item| *item == b'\n') {
|
||||
Some(position) => {
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
if buf[skip + position.saturating_sub(1)] == b'\r' {
|
||||
Some(Ok(position + 1))
|
||||
} else {
|
||||
Some(Err(position + 1))
|
||||
}
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
Some(Ok(position + 1))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_find_crlf_inclusive() {
|
||||
let tests = [
|
||||
(b"A\r".as_ref(), 0, None),
|
||||
(b"A\r\n", 0, Some(Ok(3))),
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
(b"A\n", 0, Some(Err(2))),
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
(b"A\n", 0, Some(Ok(2))),
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
(b"\n", 0, Some(Err(1))),
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
(b"\n", 0, Some(Ok(1))),
|
||||
(b"aaa\r\nA\r".as_ref(), 5, None),
|
||||
(b"aaa\r\nA\r\n", 5, Some(Ok(3))),
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
(b"aaa\r\nA\n", 5, Some(Err(2))),
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
(b"aaa\r\nA\n", 5, Some(Ok(2))),
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
(b"aaa\r\n\n", 5, Some(Err(1))),
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
(b"aaa\r\n\n", 5, Some(Ok(1))),
|
||||
];
|
||||
|
||||
for (test, skip, expected) in tests {
|
||||
let got = find_crlf_inclusive(skip, test);
|
||||
|
||||
dbg!((std::str::from_utf8(test).unwrap(), skip, &expected, &got));
|
||||
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,358 @@
|
||||
use std::io::{Error as IoError, Write};
|
||||
|
||||
use bounded_static::IntoBoundedStatic;
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use imap_codec::{
|
||||
codec::{Decode, DecodeError, Encode},
|
||||
imap_types::{
|
||||
command::Command,
|
||||
response::{Greeting, Response},
|
||||
},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use super::{find_crlf_inclusive, FramingError, FramingState};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ImapServerCodec {
|
||||
state: FramingState,
|
||||
max_literal_size: usize,
|
||||
}
|
||||
|
||||
impl ImapServerCodec {
|
||||
pub fn new(max_literal_size: usize) -> Self {
|
||||
Self {
|
||||
state: FramingState::ReadLine { to_consume_acc: 0 },
|
||||
max_literal_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ImapServerCodecError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
#[error(transparent)]
|
||||
Framing(#[from] FramingError),
|
||||
#[error("Parsing failed")]
|
||||
ParsingFailed(BytesMut),
|
||||
}
|
||||
|
||||
impl PartialEq for ImapServerCodecError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Io(error1), Self::Io(error2)) => error1.kind() == error2.kind(),
|
||||
(Self::Framing(kind1), Self::Framing(kind2)) => kind1 == kind2,
|
||||
(Self::ParsingFailed(x), Self::ParsingFailed(y)) => x == y,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
Command(Command<'static>),
|
||||
ActionRequired(Action),
|
||||
// More might be require.
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
SendLiteralAck(u32),
|
||||
SendLiteralReject(u32),
|
||||
}
|
||||
|
||||
impl Decoder for ImapServerCodec {
|
||||
type Item = Event;
|
||||
type Error = ImapServerCodecError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
loop {
|
||||
match self.state {
|
||||
FramingState::ReadLine {
|
||||
ref mut to_consume_acc,
|
||||
} => match find_crlf_inclusive(*to_consume_acc, src) {
|
||||
Some(line) => match line {
|
||||
// After skipping `to_consume_acc` bytes, we need `to_consume` more
|
||||
// bytes to form a full line (including the `\r\n`).
|
||||
Ok(to_consume) => {
|
||||
*to_consume_acc += to_consume;
|
||||
let line = &src[..*to_consume_acc];
|
||||
|
||||
// TODO: Choose the required parser.
|
||||
match Command::decode(line) {
|
||||
// We got a complete message.
|
||||
Ok((rem, cmd)) => {
|
||||
assert!(rem.is_empty());
|
||||
let cmd = cmd.into_static();
|
||||
|
||||
src.advance(*to_consume_acc);
|
||||
self.state = FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
return Ok(Some(Event::Command(cmd)));
|
||||
}
|
||||
Err(error) => match error {
|
||||
// We supposedly need more data ...
|
||||
//
|
||||
// This should not happen because a line that doesn't end
|
||||
// with a literal is always "complete" in IMAP.
|
||||
DecodeError::Incomplete => {
|
||||
unreachable!();
|
||||
}
|
||||
// We found a literal.
|
||||
DecodeError::LiteralFound { length, .. } => {
|
||||
if length as usize <= self.max_literal_size {
|
||||
src.reserve(length as usize);
|
||||
|
||||
self.state = FramingState::ReadLiteral {
|
||||
to_consume_acc: *to_consume_acc,
|
||||
length,
|
||||
};
|
||||
|
||||
return Ok(Some(Event::ActionRequired(
|
||||
Action::SendLiteralAck(length),
|
||||
)));
|
||||
} else {
|
||||
src.advance(*to_consume_acc);
|
||||
|
||||
self.state =
|
||||
FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
return Ok(Some(Event::ActionRequired(
|
||||
Action::SendLiteralReject(length),
|
||||
)));
|
||||
}
|
||||
}
|
||||
DecodeError::Failed => {
|
||||
let consumed = src.split_to(*to_consume_acc);
|
||||
self.state = FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
return Err(ImapServerCodecError::ParsingFailed(consumed));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// After skipping `to_consume_acc` bytes, we need `to_consume` more
|
||||
// bytes to form a full line (including the `\n`).
|
||||
//
|
||||
// Note: This line is missing the `\r\n` and should be discarded.
|
||||
Err(to_discard) => {
|
||||
src.advance(*to_consume_acc + to_discard);
|
||||
self.state = FramingState::ReadLine { to_consume_acc: 0 };
|
||||
|
||||
return Err(ImapServerCodecError::Framing(FramingError::NotCrLf));
|
||||
}
|
||||
},
|
||||
// More data needed.
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
FramingState::ReadLiteral {
|
||||
to_consume_acc,
|
||||
length,
|
||||
} => {
|
||||
if to_consume_acc + length as usize <= src.len() {
|
||||
self.state = FramingState::ReadLine {
|
||||
to_consume_acc: to_consume_acc + length as usize,
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<&Greeting<'_>> for ImapServerCodec {
|
||||
type Error = IoError;
|
||||
|
||||
fn encode(&mut self, item: &Greeting, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
//dst.reserve(item.len());
|
||||
let mut writer = dst.writer();
|
||||
// TODO(225): Don't use `dump` here.
|
||||
let data = item.encode().dump();
|
||||
writer.write_all(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<&Response<'_>> for ImapServerCodec {
|
||||
type Error = IoError;
|
||||
|
||||
fn encode(&mut self, item: &Response, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
//dst.reserve(item.len());
|
||||
let mut writer = dst.writer();
|
||||
// TODO(225): Don't use `dump` here.
|
||||
let data = item.encode().dump();
|
||||
writer.write_all(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bytes::BytesMut;
|
||||
use imap_codec::imap_types::{
|
||||
command::{Command, CommandBody},
|
||||
core::{AString, AtomExt, IString, Literal},
|
||||
secret::Secret,
|
||||
};
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
use imap_types::core::Tag;
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_decoder_line() {
|
||||
let tests = [
|
||||
(b"".as_ref(), Ok(None)),
|
||||
(b"a noop", Ok(None)),
|
||||
(b"\r", Ok(None)),
|
||||
(
|
||||
b"\n",
|
||||
Ok(Some(Event::Command(
|
||||
Command::new("a", CommandBody::Noop).unwrap(),
|
||||
))),
|
||||
),
|
||||
(b"", Ok(None)),
|
||||
(b"xxxx", Ok(None)),
|
||||
(
|
||||
b"\r\n",
|
||||
Err(ImapServerCodecError::ParsingFailed(BytesMut::from(
|
||||
b"xxxx\r\n".as_ref(),
|
||||
))),
|
||||
),
|
||||
];
|
||||
|
||||
let mut src = BytesMut::new();
|
||||
let mut codec = ImapServerCodec::new(1024);
|
||||
|
||||
for (test, expected) in tests {
|
||||
src.extend_from_slice(test);
|
||||
let got = codec.decode(&mut src);
|
||||
|
||||
assert_eq!(expected, got);
|
||||
|
||||
dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoder_literal() {
|
||||
let tests = [
|
||||
(b"".as_ref(), Ok(None)),
|
||||
(b"a login", Ok(None)),
|
||||
(b" {", Ok(None)),
|
||||
(b"5", Ok(None)),
|
||||
(b"}", Ok(None)),
|
||||
(
|
||||
b"\r\n",
|
||||
Ok(Some(Event::ActionRequired(Action::SendLiteralAck(5)))),
|
||||
),
|
||||
(b"a", Ok(None)),
|
||||
(b"l", Ok(None)),
|
||||
(b"i", Ok(None)),
|
||||
(b"ce", Ok(None)),
|
||||
(b" ", Ok(None)),
|
||||
(
|
||||
b"password\r\n",
|
||||
Ok(Some(Event::Command(
|
||||
Command::new(
|
||||
"a",
|
||||
CommandBody::Login {
|
||||
username: AString::String(IString::Literal(
|
||||
Literal::try_from(b"alice".as_ref()).unwrap(),
|
||||
)),
|
||||
password: Secret::new(AString::Atom(
|
||||
AtomExt::try_from("password").unwrap(),
|
||||
)),
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
))),
|
||||
),
|
||||
];
|
||||
|
||||
let mut src = BytesMut::new();
|
||||
let mut codec = ImapServerCodec::new(1024);
|
||||
|
||||
for (test, expected) in tests {
|
||||
src.extend_from_slice(test);
|
||||
let got = codec.decode(&mut src);
|
||||
|
||||
dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
|
||||
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoder_error() {
|
||||
let tests = [
|
||||
(
|
||||
b"xxx\r\n".as_ref(),
|
||||
Err(ImapServerCodecError::ParsingFailed(BytesMut::from(
|
||||
b"xxx\r\n".as_ref(),
|
||||
))),
|
||||
),
|
||||
(
|
||||
b"a noop\n",
|
||||
#[cfg(not(feature = "quirk_crlf_relaxed"))]
|
||||
Err(ImapServerCodecError::Framing(FramingError::NotCrLf)),
|
||||
#[cfg(feature = "quirk_crlf_relaxed")]
|
||||
Ok(Some(Event::Command(Command {
|
||||
tag: Tag::unvalidated("a"),
|
||||
body: CommandBody::Noop,
|
||||
}))),
|
||||
),
|
||||
(
|
||||
b"a login alice {16}\r\n",
|
||||
Ok(Some(Event::ActionRequired(Action::SendLiteralAck(16)))),
|
||||
),
|
||||
(
|
||||
b"aaaaaaaaaaaaaaaa\r\n",
|
||||
Ok(Some(Event::Command(
|
||||
Command::new(
|
||||
"a",
|
||||
CommandBody::login("alice", Literal::try_from("aaaaaaaaaaaaaaaa").unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
))),
|
||||
),
|
||||
(
|
||||
b"a login alice {17}\r\n",
|
||||
Ok(Some(Event::ActionRequired(Action::SendLiteralReject(17)))),
|
||||
),
|
||||
(
|
||||
b"a login alice {1-}\r\n",
|
||||
Err(ImapServerCodecError::ParsingFailed(BytesMut::from(
|
||||
b"a login alice {1-}\r\n".as_ref(),
|
||||
))),
|
||||
),
|
||||
(
|
||||
// Ohhhhhh, IMAP :-/
|
||||
b"a login alice }\r\n",
|
||||
Ok(Some(Event::Command(
|
||||
Command::new("a", CommandBody::login("alice", "}").unwrap()).unwrap(),
|
||||
))),
|
||||
),
|
||||
];
|
||||
|
||||
let mut src = BytesMut::new();
|
||||
let mut codec = ImapServerCodec::new(16);
|
||||
|
||||
for (test, expected) in tests {
|
||||
src.extend_from_slice(test);
|
||||
dbg!(&src, &codec);
|
||||
let got = codec.decode(&mut src);
|
||||
dbg!((std::str::from_utf8(test).unwrap(), &expected, &got));
|
||||
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,268 @@
|
||||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2023 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use melib::{backends::ImapType, futures, smol, AccountSettings, BackendEventConsumer, Result};
|
||||
use std::net::TcpListener;
|
||||
use std::path::Path;
|
||||
use test_binary::build_test_binary;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TestError {}
|
||||
|
||||
impl<Err: std::fmt::Display> From<Err> for TestError {
|
||||
#[track_caller]
|
||||
fn from(err: Err) -> Self {
|
||||
panic!("error: {}: {}", std::any::type_name::<Err>(), err);
|
||||
}
|
||||
}
|
||||
|
||||
type TestResult = std::result::Result<(), TestError>;
|
||||
|
||||
struct Server {
|
||||
child: std::process::Child,
|
||||
}
|
||||
|
||||
impl From<std::process::Child> for Server {
|
||||
fn from(child: std::process::Child) -> Self {
|
||||
Self { child }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
_ = self.child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn stunnel_conf_gen(tls_port: u16, connect_port: u16, cert: &Path, key: &Path) -> String {
|
||||
format!(
|
||||
r#"
|
||||
[imaps]
|
||||
accept = {tls_port}
|
||||
connect = {connect_port}
|
||||
cert = {cert}
|
||||
key = {key}
|
||||
"#,
|
||||
cert = cert.display(),
|
||||
key = key.display()
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn imap_session(acc: AccountSettings) -> TestResult {
|
||||
let imap = ImapType::new(
|
||||
&acc,
|
||||
Box::new(|_| true),
|
||||
BackendEventConsumer::new(std::sync::Arc::new(|_, _| ())),
|
||||
)?;
|
||||
let _is_online = imap.is_online()?.await?;
|
||||
let _mailboxes = imap.mailboxes()?.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_available_port() -> Result<u16> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||
Ok(listener.local_addr()?.port())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn imap_conn_setup(tempdir: &tempfile::TempDir, stunnel: bool) -> TestResult {
|
||||
let exec_script = |path: &Path, cwd: &Path| -> TestResult {
|
||||
let mut script = std::process::Command::new(path).current_dir(cwd).spawn()?;
|
||||
script.wait()?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let ex = smol::Executor::new();
|
||||
futures::executor::block_on(ex.run(futures::future::pending::<()>()));
|
||||
});
|
||||
|
||||
// 1. make localhost cert
|
||||
|
||||
exec_script(
|
||||
&Path::new("./tests/bins/make_localhost_cert.sh").canonicalize()?,
|
||||
tempdir.path(),
|
||||
)?;
|
||||
|
||||
if stunnel {
|
||||
// 2. build stunnel
|
||||
|
||||
std::fs::copy(
|
||||
Path::new("./tests/bins/build_static_stunnel_binary.sh").canonicalize()?,
|
||||
&tempdir.path().join("build_static_stunnel_binary.sh"),
|
||||
)?;
|
||||
std::fs::copy(
|
||||
Path::new("./tests/bins/stunnel-5.70.tar.xz").canonicalize()?,
|
||||
&tempdir.path().join("stunnel-5.70.tar.xz"),
|
||||
)?;
|
||||
|
||||
exec_script(
|
||||
&tempdir
|
||||
.path()
|
||||
.join("build_static_stunnel_binary.sh")
|
||||
.canonicalize()?,
|
||||
tempdir.path(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_plaintext_connection() -> TestResult {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
imap_conn_setup(&tempdir, false)?;
|
||||
|
||||
let port = get_available_port()?;
|
||||
|
||||
let test_bin_path =
|
||||
build_test_binary("tokio-server", "tests/bins").expect("error building test binary");
|
||||
|
||||
let test_bin_subproc: Server = std::process::Command::new(test_bin_path)
|
||||
.arg(&format!("127.0.0.1:{}", port))
|
||||
.spawn()
|
||||
.expect("error running test binary")
|
||||
.into();
|
||||
|
||||
let insecure = AccountSettings {
|
||||
extra: [
|
||||
("server_hostname".to_string(), "localhost".to_string()),
|
||||
("server_username".to_string(), "alice".to_string()),
|
||||
("server_password".to_string(), "password".to_string()),
|
||||
("server_port".to_string(), port.to_string()),
|
||||
("use_starttls".to_string(), "false".to_string()),
|
||||
("use_tls".to_string(), "false".to_string()),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eprintln!("• running mock plaintext IMAP server at 127.0.0.1:{port}...");
|
||||
futures::executor::block_on(imap_session(insecure))?;
|
||||
|
||||
drop(test_bin_subproc);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_starttls_connection() -> TestResult {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
imap_conn_setup(&tempdir, false)?;
|
||||
|
||||
let port = get_available_port()?;
|
||||
let test_bin_path =
|
||||
build_test_binary("tokio-server", "tests/bins").expect("error building test binary");
|
||||
|
||||
let test_bin_subproc: Server = std::process::Command::new(test_bin_path)
|
||||
.arg(&format!("127.0.0.1:{}", port))
|
||||
.spawn()
|
||||
.expect("error running test binary")
|
||||
.into();
|
||||
|
||||
let starttls = AccountSettings {
|
||||
extra: [
|
||||
("server_hostname".to_string(), "localhost".to_string()),
|
||||
("server_username".to_string(), "alice".to_string()),
|
||||
("server_password".to_string(), "password".to_string()),
|
||||
("server_port".to_string(), port.to_string()),
|
||||
("use_starttls".to_string(), "true".to_string()),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eprintln!("• running mock IMAP server at 127.0.0.1:{port} and connecting with STARTTLS...");
|
||||
futures::executor::block_on(imap_session(starttls))?;
|
||||
|
||||
drop(test_bin_subproc);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_tls_connection() -> TestResult {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
imap_conn_setup(&tempdir, true)?;
|
||||
let port = get_available_port()?;
|
||||
let tls_port = get_available_port()?;
|
||||
|
||||
let stunnel = tempdir.path().join("stunnel");
|
||||
assert!(stunnel.is_file());
|
||||
let stunnel_conf = tempdir.path().join("stunnel.conf");
|
||||
std::fs::write(
|
||||
&stunnel_conf,
|
||||
stunnel_conf_gen(
|
||||
tls_port,
|
||||
port,
|
||||
&tempdir.path().join("localhost.crt"),
|
||||
&tempdir.path().join("localhost.key"),
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
let test_bin_path =
|
||||
build_test_binary("tokio-server", "tests/bins").expect("error building test binary");
|
||||
|
||||
let test_bin_subproc: Server = std::process::Command::new(test_bin_path)
|
||||
.arg(&format!("127.0.0.1:{}", port))
|
||||
.spawn()
|
||||
.expect("error running test binary")
|
||||
.into();
|
||||
|
||||
let tls_subproc: Server = std::process::Command::new(&stunnel)
|
||||
.arg(&stunnel_conf)
|
||||
.spawn()
|
||||
.expect("error running test binary")
|
||||
.into();
|
||||
|
||||
let set = AccountSettings {
|
||||
extra: [
|
||||
("server_hostname".to_string(), "127.0.0.1".to_string()),
|
||||
("server_username".to_string(), "alice".to_string()),
|
||||
("server_password".to_string(), "password".to_string()),
|
||||
("server_port".to_string(), tls_port.to_string()),
|
||||
("use_starttls".to_string(), "false".to_string()),
|
||||
("use_tls".to_string(), "true".to_string()),
|
||||
(
|
||||
"danger_accept_invalid_certs".to_string(),
|
||||
"true".to_string(),
|
||||
),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eprintln!("• running mock IMAP server at 127.0.0.1:{port} and a TLS tunnel at 127.0.0.1:{tls_port}...");
|
||||
futures::executor::block_on(imap_session(set))?;
|
||||
drop(tls_subproc);
|
||||
drop(test_bin_subproc);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue