#![allow(dead_code)]
use std::{
io::{Read, Write},
num::NonZeroU32,
};
use io_imap::{
codec::fragmentizer::Fragmentizer,
coroutine::*,
rfc3501::{append::*, fetch::*, fetch_stream::*, greeting::*, login::*, logout::*, select::*},
rfc5256::sort::*,
types::{
core::Vec1,
extensions::sort::{SortCriterion, SortKey},
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
flag::Flag,
response::Capability,
search::SearchKey,
sequence::{SeqOrUid, SequenceSet},
},
};
use pimalaya_stream::{std::stream::StreamStd, tls::Tls};
const FRAGMENTIZER_MAX_MESSAGE_SIZE: u32 = 100 * 1024 * 1024;
const SUBJECT: &[u8] = b"io-imap integration test";
pub fn run_imaps(host: &str, port: u16, username: &str, password: &str) {
let _ = env_logger::try_init();
let stream = StreamStd::connect_tls(host, port, &Tls::default()).expect("TLS connect");
run(stream, username, password)
}
pub fn run_imap(host: &str, port: u16, username: &str, password: &str) {
let _ = env_logger::try_init();
let stream = StreamStd::connect_tcp(host, port).expect("TCP connect");
run(stream, username, password)
}
fn run(mut stream: impl Read + Write, username: &str, password: &str) {
let mut buf = [0u8; 16 * 1024];
let mut fragmentizer = Fragmentizer::new(FRAGMENTIZER_MAX_MESSAGE_SIZE);
let mut coroutine = ImapGreetingGet::new(ImapGreetingGetOptions {
ensure_capabilities: true,
});
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(_)) => break,
ImapCoroutineState::Complete(Err(err)) => panic!("GREETING: {err}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("greeting read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("greeting write");
arg = None;
}
}
}
let opts = ImapLoginOptions {
ensure_capabilities: true,
auto_id: None,
};
let mut coroutine = ImapLogin::new(username, password, opts).expect("valid credentials");
let mut arg: Option<&[u8]> = None;
let capabilities = loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(capabilities)) => break capabilities,
ImapCoroutineState::Complete(Err(err)) => panic!("LOGIN: {err}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("login read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("login write");
arg = None;
}
}
};
let has_sort = capabilities
.iter()
.any(|capability| matches!(capability, Capability::Sort(_)));
let mut coroutine = ImapMailboxSelect::new(
"INBOX".try_into().unwrap(),
ImapMailboxSelectOptions::default(),
);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(_)) => break,
ImapCoroutineState::Complete(Err(err)) => panic!("SELECT: {err:?}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("select read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("select write");
arg = None;
}
}
}
let message = b"Date: Mon, 1 Jan 2024 00:00:00 +0000\r\n\
From: io-imap <test@pimalaya.org>\r\n\
To: io-imap <test@pimalaya.org>\r\n\
Subject: io-imap integration test\r\n\
\r\n\
Hello from the io-imap integration test.\r\n";
let opts = ImapMessageAppendOptions {
flags: vec![Flag::Seen],
..Default::default()
};
let mut coroutine = ImapMessageAppend::new("INBOX".try_into().unwrap(), message.to_vec(), opts);
let mut arg: Option<&[u8]> = None;
let (exists, appenduid) = loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(out)) => break out,
ImapCoroutineState::Complete(Err(err)) => panic!("APPEND: {err}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("append read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("append write");
arg = None;
}
}
};
let (id, uid) = match appenduid {
Some((_uid_validity, uid)) => (NonZeroU32::new(uid).expect("non-zero APPENDUID"), true),
None => {
let seq = exists.expect("APPEND returned neither APPENDUID nor EXISTS");
(NonZeroU32::new(seq).expect("non-zero EXISTS"), false)
}
};
let items =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]);
let mut coroutine = ImapMessageFetch::new(
SequenceSet::from(SeqOrUid::from(id)),
items,
ImapMessageFetchOptions {
uid,
..Default::default()
},
);
let mut arg: Option<&[u8]> = None;
let fetched = loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(map)) => break map,
ImapCoroutineState::Complete(Err(err)) => panic!("FETCH: {err}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("fetch read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("fetch write");
arg = None;
}
}
};
assert!(!fetched.is_empty(), "buffered FETCH returned no message");
let mut coroutine = ImapMessageFetchStream::new(id, uid);
let mut chunk = [0u8; 64];
let mut body: Vec<u8> = Vec::new();
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(())) => break,
ImapCoroutineState::Complete(Err(err)) => panic!("FETCH stream: {err}"),
ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsRead) => {
let n = stream.read(&mut chunk).expect("fetch stream read");
arg = Some(&chunk[..n]);
}
ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("fetch stream write");
arg = None;
}
ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::BodyChunk(bytes)) => {
body.extend_from_slice(&bytes);
arg = None;
}
ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream { len }) => {
let mut remaining = len as usize;
while remaining > 0 {
let want = remaining.min(chunk.len());
let n = stream
.read(&mut chunk[..want])
.expect("fetch stream body read");
if n == 0 {
break;
}
body.extend_from_slice(&chunk[..n]);
remaining -= n;
}
arg = (remaining > 0).then_some(&[]);
}
}
}
assert!(
body.windows(SUBJECT.len()).any(|window| window == SUBJECT),
"streamed body missing the appended subject"
);
let sort_criteria = Vec1::try_from(vec![SortCriterion {
reverse: true,
key: SortKey::Date,
}])
.unwrap();
let search_criteria = Vec1::try_from(vec![SearchKey::All]).unwrap();
let mut coroutine = ImapMessageSort::new(
sort_criteria,
search_criteria,
ImapMessageSortOptions {
uid: true,
fallback: !has_sort,
},
);
let mut arg: Option<&[u8]> = None;
let ids = loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(ids)) => break ids,
ImapCoroutineState::Complete(Err(err)) => panic!("SORT: {err}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("sort read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("sort write");
arg = None;
}
}
};
assert!(!ids.is_empty(), "SORT returned no ids after APPEND");
let mut coroutine = ImapLogout::new();
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(&mut fragmentizer, arg.take()) {
ImapCoroutineState::Complete(Ok(())) => break,
ImapCoroutineState::Complete(Err(err)) => panic!("LOGOUT: {err}"),
ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
let n = stream.read(&mut buf).expect("logout read");
arg = Some(&buf[..n]);
}
ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
stream.write_all(&bytes).expect("logout write");
arg = None;
}
}
}
}