523 lines
14 KiB
Rust
523 lines
14 KiB
Rust
use anyhow::{bail, Result};
|
|
use std::io::Write;
|
|
use std::net::TcpStream;
|
|
use std::thread;
|
|
|
|
use crate::common::constants::*;
|
|
use crate::common::*;
|
|
|
|
/// These fragments are not a generic IMAP client
|
|
/// but specialized to our specific tests. They can't take
|
|
/// arbitrary values, only enum for which the code is known
|
|
/// to be correct. The idea is that the generated message is more
|
|
/// or less hardcoded by the developer, so its clear what's expected,
|
|
/// and not generated by a library. Also don't use vector of enum,
|
|
/// as it again introduce some kind of genericity we try so hard to avoid:
|
|
/// instead add a dedicated enum, for example "All" or anything relaevent that would
|
|
/// describe your list and then hardcode it in your fragment.
|
|
/// DON'T. TRY. TO. BE. GENERIC. HERE.
|
|
|
|
pub fn connect(imap: &mut TcpStream) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..4], &b"* OK"[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub enum Account {
|
|
Alice,
|
|
}
|
|
|
|
pub enum Extension {
|
|
None,
|
|
Unselect,
|
|
Move,
|
|
Condstore,
|
|
LiteralPlus,
|
|
Idle,
|
|
}
|
|
|
|
pub enum Enable {
|
|
Utf8Accept,
|
|
CondStore,
|
|
All,
|
|
}
|
|
|
|
pub enum Mailbox {
|
|
Inbox,
|
|
Archive,
|
|
Drafts,
|
|
}
|
|
|
|
pub enum Flag {
|
|
Deleted,
|
|
Important,
|
|
}
|
|
|
|
pub enum Email {
|
|
Basic,
|
|
Multipart,
|
|
}
|
|
|
|
pub enum Selection {
|
|
FirstId,
|
|
SecondId,
|
|
All,
|
|
}
|
|
|
|
pub enum SelectMod {
|
|
None,
|
|
Condstore,
|
|
}
|
|
|
|
pub enum StoreAction {
|
|
AddFlags,
|
|
DelFlags,
|
|
SetFlags,
|
|
AddFlagsSilent,
|
|
DelFlagsSilent,
|
|
SetFlagsSilent,
|
|
}
|
|
|
|
pub enum StoreMod {
|
|
None,
|
|
UnchangedSince(u64),
|
|
}
|
|
|
|
pub enum FetchKind {
|
|
Rfc822,
|
|
Rfc822Size,
|
|
}
|
|
|
|
pub enum FetchMod {
|
|
None,
|
|
ChangedSince(u64),
|
|
}
|
|
|
|
pub enum SearchKind<'a> {
|
|
Text(&'a str),
|
|
ModSeq(u64),
|
|
}
|
|
|
|
pub enum StatusKind {
|
|
UidNext,
|
|
HighestModSeq,
|
|
}
|
|
|
|
pub fn capability(imap: &mut TcpStream, ext: Extension) -> Result<()> {
|
|
imap.write(&b"5 capability\r\n"[..])?;
|
|
|
|
let maybe_ext = match ext {
|
|
Extension::None => None,
|
|
Extension::Unselect => Some("UNSELECT"),
|
|
Extension::Move => Some("MOVE"),
|
|
Extension::Condstore => Some("CONDSTORE"),
|
|
Extension::LiteralPlus => Some("LITERAL+"),
|
|
Extension::Idle => Some("IDLE"),
|
|
};
|
|
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
let read = read_lines(imap, &mut buffer, Some(&b"5 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
assert!(srv_msg.contains("IMAP4REV1"));
|
|
if let Some(ext) = maybe_ext {
|
|
assert!(srv_msg.contains(ext));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn login(imap: &mut TcpStream, account: Account) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
assert!(matches!(account, Account::Alice));
|
|
imap.write(&b"10 login alice hunter2\r\n"[..])?;
|
|
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..5], &b"10 OK"[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn login_with_literal(imap: &mut TcpStream, account: Account) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
assert!(matches!(account, Account::Alice));
|
|
imap.write(&b"10 login {5+}\r\nalice {7+}\r\nhunter2\r\n"[..])?;
|
|
let _read = read_lines(imap, &mut buffer, Some(&b"10 OK"[..]))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn create_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
let mbx_str = match mbx {
|
|
Mailbox::Inbox => "INBOX",
|
|
Mailbox::Archive => "ArchiveCustom",
|
|
Mailbox::Drafts => "DraftsCustom",
|
|
};
|
|
|
|
let cmd = format!("15 create {}\r\n", mbx_str);
|
|
imap.write(cmd.as_bytes())?;
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..12], &b"15 OK CREATE"[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn select(imap: &mut TcpStream, mbx: Mailbox, modifier: SelectMod) -> Result<String> {
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
|
|
let mbx_str = match mbx {
|
|
Mailbox::Inbox => "INBOX",
|
|
Mailbox::Archive => "ArchiveCustom",
|
|
Mailbox::Drafts => "DraftsCustom",
|
|
};
|
|
|
|
let mod_str = match modifier {
|
|
SelectMod::Condstore => " (CONDSTORE)",
|
|
SelectMod::None => "",
|
|
};
|
|
|
|
imap.write(format!("20 select {}{}\r\n", mbx_str, mod_str).as_bytes())?;
|
|
|
|
let read = read_lines(imap, &mut buffer, Some(&b"20 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
|
|
Ok(srv_msg.to_string())
|
|
}
|
|
|
|
pub fn unselect(imap: &mut TcpStream) -> Result<()> {
|
|
imap.write(&b"70 unselect\r\n"[..])?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let _read = read_lines(imap, &mut buffer, Some(&b"70 OK"[..]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn check(imap: &mut TcpStream) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
imap.write(&b"21 check\r\n"[..])?;
|
|
let _read = read_lines(imap, &mut buffer, Some(&b"21 OK"[..]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn status(imap: &mut TcpStream, mbx: Mailbox, sk: StatusKind) -> Result<String> {
|
|
let mbx_str = match mbx {
|
|
Mailbox::Inbox => "INBOX",
|
|
Mailbox::Archive => "ArchiveCustom",
|
|
Mailbox::Drafts => "DraftsCustom",
|
|
};
|
|
let sk_str = match sk {
|
|
StatusKind::UidNext => "(UIDNEXT)",
|
|
StatusKind::HighestModSeq => "(HIGHESTMODSEQ)",
|
|
};
|
|
imap.write(format!("25 STATUS {} {}\r\n", mbx_str, sk_str).as_bytes())?;
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
let read = read_lines(imap, &mut buffer, Some(&b"25 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
|
|
Ok(srv_msg.to_string())
|
|
}
|
|
|
|
pub fn lmtp_handshake(lmtp: &mut TcpStream) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
let _read = read_lines(lmtp, &mut buffer, None)?;
|
|
assert_eq!(&buffer[..4], &b"220 "[..]);
|
|
|
|
lmtp.write(&b"LHLO example.tld\r\n"[..])?;
|
|
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 "[..]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn lmtp_deliver_email(lmtp: &mut TcpStream, email_type: Email) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
|
|
let email = match email_type {
|
|
Email::Basic => EMAIL2,
|
|
Email::Multipart => EMAIL1,
|
|
};
|
|
lmtp.write(&b"MAIL FROM:<bob@example.tld>\r\n"[..])?;
|
|
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?;
|
|
|
|
lmtp.write(&b"RCPT TO:<alice@example.tld>\r\n"[..])?;
|
|
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.1.5"[..]))?;
|
|
|
|
lmtp.write(&b"DATA\r\n"[..])?;
|
|
let _read = read_lines(lmtp, &mut buffer, Some(&b"354 "[..]))?;
|
|
|
|
lmtp.write(email)?;
|
|
lmtp.write(&b"\r\n.\r\n"[..])?;
|
|
let _read = read_lines(lmtp, &mut buffer, Some(&b"250 2.0.0"[..]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn noop_exists(imap: &mut TcpStream, must_exists: u32) -> Result<()> {
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
|
|
let mut max_retry = 20;
|
|
loop {
|
|
max_retry -= 1;
|
|
imap.write(&b"30 NOOP\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"30 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
|
|
for line in srv_msg.lines() {
|
|
if line.contains("EXISTS") {
|
|
let got = read_first_u32(line)?;
|
|
if got == must_exists {
|
|
// Done
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
if max_retry <= 0 {
|
|
// Failed
|
|
bail!("no more retry");
|
|
}
|
|
|
|
thread::sleep(SMALL_DELAY);
|
|
}
|
|
}
|
|
|
|
pub fn fetch(
|
|
imap: &mut TcpStream,
|
|
selection: Selection,
|
|
kind: FetchKind,
|
|
modifier: FetchMod,
|
|
) -> Result<String> {
|
|
let mut buffer: [u8; 65535] = [0; 65535];
|
|
|
|
let sel_str = match selection {
|
|
Selection::FirstId => "1",
|
|
Selection::SecondId => "2",
|
|
Selection::All => "1:*",
|
|
};
|
|
|
|
let kind_str = match kind {
|
|
FetchKind::Rfc822 => "RFC822",
|
|
FetchKind::Rfc822Size => "RFC822.SIZE",
|
|
};
|
|
|
|
let mod_str = match modifier {
|
|
FetchMod::None => "".into(),
|
|
FetchMod::ChangedSince(val) => format!(" (CHANGEDSINCE {})", val),
|
|
};
|
|
|
|
imap.write(format!("40 fetch {} {}{}\r\n", sel_str, kind_str, mod_str).as_bytes())?;
|
|
|
|
let read = read_lines(imap, &mut buffer, Some(&b"40 OK FETCH"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
|
|
Ok(srv_msg.to_string())
|
|
}
|
|
|
|
pub fn copy(imap: &mut TcpStream, selection: Selection, to: Mailbox) -> Result<()> {
|
|
let mut buffer: [u8; 65535] = [0; 65535];
|
|
assert!(matches!(selection, Selection::FirstId));
|
|
assert!(matches!(to, Mailbox::Archive));
|
|
|
|
imap.write(&b"45 copy 1 ArchiveCustom\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..5], &b"45 OK"[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn append_email(imap: &mut TcpStream, content: Email) -> Result<()> {
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
|
|
let ref_mail = match content {
|
|
Email::Multipart => EMAIL1,
|
|
Email::Basic => EMAIL2,
|
|
};
|
|
|
|
let append_cmd = format!("47 append inbox (\\Seen) {{{}}}\r\n", ref_mail.len());
|
|
println!("append cmd: {}", append_cmd);
|
|
imap.write(append_cmd.as_bytes())?;
|
|
|
|
// wait for continuation
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(read[0], b'+');
|
|
|
|
// write our stuff
|
|
imap.write(ref_mail)?;
|
|
imap.write(&b"\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..5], &b"47 OK"[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn search(imap: &mut TcpStream, sk: SearchKind) -> Result<String> {
|
|
let sk_str = match sk {
|
|
SearchKind::Text(x) => format!("TEXT \"{}\"", x),
|
|
SearchKind::ModSeq(x) => format!("MODSEQ {}", x),
|
|
};
|
|
imap.write(format!("55 SEARCH {}\r\n", sk_str).as_bytes())?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let read = read_lines(imap, &mut buffer, Some(&b"55 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
Ok(srv_msg.to_string())
|
|
}
|
|
|
|
pub fn store(
|
|
imap: &mut TcpStream,
|
|
sel: Selection,
|
|
flag: Flag,
|
|
action: StoreAction,
|
|
modifier: StoreMod,
|
|
) -> Result<String> {
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
|
|
let seq = match sel {
|
|
Selection::FirstId => "1",
|
|
Selection::SecondId => "2",
|
|
Selection::All => "1:*",
|
|
};
|
|
|
|
let modif = match modifier {
|
|
StoreMod::None => "".into(),
|
|
StoreMod::UnchangedSince(val) => format!(" (UNCHANGEDSINCE {})", val),
|
|
};
|
|
|
|
let flags_str = match flag {
|
|
Flag::Deleted => "(\\Deleted)",
|
|
Flag::Important => "(\\Important)",
|
|
};
|
|
|
|
let action_str = match action {
|
|
StoreAction::AddFlags => "+FLAGS",
|
|
StoreAction::DelFlags => "-FLAGS",
|
|
StoreAction::SetFlags => "FLAGS",
|
|
StoreAction::AddFlagsSilent => "+FLAGS.SILENT",
|
|
StoreAction::DelFlagsSilent => "-FLAGS.SILENT",
|
|
StoreAction::SetFlagsSilent => "FLAGS.SILENT",
|
|
};
|
|
|
|
imap.write(format!("57 STORE {}{} {} {}\r\n", seq, modif, action_str, flags_str).as_bytes())?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"57 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
Ok(srv_msg.to_string())
|
|
}
|
|
|
|
pub fn expunge(imap: &mut TcpStream) -> Result<()> {
|
|
imap.write(&b"60 expunge\r\n"[..])?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let _read = read_lines(imap, &mut buffer, Some(&b"60 OK EXPUNGE"[..]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn rename_mailbox(imap: &mut TcpStream, from: Mailbox, to: Mailbox) -> Result<()> {
|
|
assert!(matches!(from, Mailbox::Archive));
|
|
assert!(matches!(to, Mailbox::Drafts));
|
|
|
|
imap.write(&b"70 rename ArchiveCustom DraftsCustom\r\n"[..])?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..5], &b"70 OK"[..]);
|
|
|
|
imap.write(&b"71 list \"\" *\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"71 OK LIST"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
assert!(!srv_msg.contains(" ArchiveCustom\r\n"));
|
|
assert!(srv_msg.contains(" INBOX\r\n"));
|
|
assert!(srv_msg.contains(" DraftsCustom\r\n"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn delete_mailbox(imap: &mut TcpStream, mbx: Mailbox) -> Result<()> {
|
|
let mbx_str = match mbx {
|
|
Mailbox::Inbox => "INBOX",
|
|
Mailbox::Archive => "ArchiveCustom",
|
|
Mailbox::Drafts => "DraftsCustom",
|
|
};
|
|
let cmd = format!("80 delete {}\r\n", mbx_str);
|
|
|
|
imap.write(cmd.as_bytes())?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..5], &b"80 OK"[..]);
|
|
|
|
imap.write(&b"81 list \"\" *\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"81 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
assert!(srv_msg.contains(" INBOX\r\n"));
|
|
assert!(!srv_msg.contains(format!(" {}\r\n", mbx_str).as_str()));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn close(imap: &mut TcpStream) -> Result<()> {
|
|
imap.write(&b"60 close\r\n"[..])?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let _read = read_lines(imap, &mut buffer, Some(&b"60 OK"[..]))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn r#move(imap: &mut TcpStream, selection: Selection, to: Mailbox) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
assert!(matches!(to, Mailbox::Archive));
|
|
assert!(matches!(selection, Selection::FirstId));
|
|
|
|
imap.write(&b"35 move 1 ArchiveCustom\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"35 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
assert!(srv_msg.contains("* 1 EXPUNGE"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn enable(imap: &mut TcpStream, ask: Enable, done: Option<Enable>) -> Result<()> {
|
|
let mut buffer: [u8; 6000] = [0; 6000];
|
|
assert!(matches!(ask, Enable::Utf8Accept));
|
|
|
|
imap.write(&b"36 enable UTF8=ACCEPT\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"36 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
match done {
|
|
None => assert_eq!(srv_msg.lines().count(), 1),
|
|
Some(Enable::Utf8Accept) => {
|
|
assert_eq!(srv_msg.lines().count(), 2);
|
|
assert!(srv_msg.contains("* ENABLED UTF8=ACCEPT"));
|
|
}
|
|
_ => unimplemented!(),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn start_idle(imap: &mut TcpStream) -> Result<()> {
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
imap.write(&b"98 IDLE\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(read[0], b'+');
|
|
Ok(())
|
|
}
|
|
|
|
pub fn stop_idle(imap: &mut TcpStream) -> Result<String> {
|
|
let mut buffer: [u8; 16536] = [0; 16536];
|
|
imap.write(&b"DONE\r\n"[..])?;
|
|
let read = read_lines(imap, &mut buffer, Some(&b"98 OK"[..]))?;
|
|
let srv_msg = std::str::from_utf8(read)?;
|
|
Ok(srv_msg.to_string())
|
|
}
|
|
|
|
pub fn logout(imap: &mut TcpStream) -> Result<()> {
|
|
imap.write(&b"99 logout\r\n"[..])?;
|
|
let mut buffer: [u8; 1500] = [0; 1500];
|
|
let read = read_lines(imap, &mut buffer, None)?;
|
|
assert_eq!(&read[..5], &b"* BYE"[..]);
|
|
Ok(())
|
|
}
|