Files
inbuxa-migrate/tests/integration/imap_client.rs
T
2026-05-29 18:02:15 +02:00

284 lines
8.6 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use super::error::{ContainerError, ContainerResult};
use super::layouts::MailboxSpec;
pub struct ImapSeed {
reader: BufReader<TcpStream>,
writer: TcpStream,
tag_seq: u32,
pub separator: char,
}
impl ImapSeed {
pub fn connect(host: &str, port: u16) -> ContainerResult<Self> {
let deadline = std::time::Instant::now() + Duration::from_secs(30);
let stream = loop {
match TcpStream::connect((host, port)) {
Ok(s) => break s,
Err(e) => {
if std::time::Instant::now() >= deadline {
return Err(e.into());
}
std::thread::sleep(Duration::from_millis(250));
}
}
};
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
stream.set_write_timeout(Some(Duration::from_secs(30)))?;
let writer = stream.try_clone()?;
let mut me = Self {
reader: BufReader::new(stream),
writer,
tag_seq: 0,
separator: '/',
};
let greeting = me.read_line()?;
if !greeting.starts_with("* OK") && !greeting.starts_with("* PREAUTH") {
return Err(ContainerError::Protocol(format!(
"imap greeting unexpected: {greeting}"
)));
}
Ok(me)
}
fn next_tag(&mut self) -> String {
self.tag_seq += 1;
format!("a{:04}", self.tag_seq)
}
fn read_line(&mut self) -> ContainerResult<String> {
let mut line = String::new();
let n = self.reader.read_line(&mut line)?;
if n == 0 {
return Err(ContainerError::Protocol("imap eof".to_owned()));
}
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
}
Ok(line)
}
fn read_tagged(&mut self, tag: &str) -> ContainerResult<Vec<String>> {
let mut untagged = Vec::new();
loop {
let line = self.read_line()?;
if let Some(rest) = line.strip_prefix(&format!("{tag} ")) {
if rest.starts_with("OK") {
return Ok(untagged);
}
return Err(ContainerError::Protocol(format!(
"imap tagged failure: {line}"
)));
}
untagged.push(line);
}
}
fn send(&mut self, line: &str) -> ContainerResult<()> {
self.writer.write_all(line.as_bytes())?;
self.writer.write_all(b"\r\n")?;
self.writer.flush()?;
Ok(())
}
pub fn login(&mut self, user: &str, password: &str) -> ContainerResult<()> {
let tag = self.next_tag();
let escaped_user = quote(user);
let escaped_pw = quote(password);
self.send(&format!("{tag} LOGIN {escaped_user} {escaped_pw}"))?;
self.read_tagged(&tag)?;
Ok(())
}
pub fn discover_separator(&mut self) -> ContainerResult<char> {
let tag = self.next_tag();
self.send(&format!(r#"{tag} LIST "" """#))?;
let untagged = self.read_tagged(&tag)?;
for line in &untagged {
if let Some(rest) = line.strip_prefix("* LIST ")
&& let Some(open) = rest.find('"')
&& let Some(close) = rest[open + 1..].find('"')
{
let sep = &rest[open + 1..open + 1 + close];
if let Some(c) = sep.chars().next() {
self.separator = c;
return Ok(c);
}
}
}
Ok('/')
}
pub fn create(&mut self, full_path: &str) -> ContainerResult<()> {
let tag = self.next_tag();
self.send(&format!("{tag} CREATE {}", quote(full_path)))?;
match self.read_tagged(&tag) {
Ok(_) => Ok(()),
Err(ContainerError::Protocol(msg)) if msg.contains("already exists") => Ok(()),
Err(e) => Err(e),
}
}
pub fn subscribe(&mut self, full_path: &str) -> ContainerResult<()> {
let tag = self.next_tag();
self.send(&format!("{tag} SUBSCRIBE {}", quote(full_path)))?;
let _ = self.read_tagged(&tag);
Ok(())
}
pub fn append(&mut self, mailbox: &str, message: &[u8]) -> ContainerResult<()> {
self.append_with_flags(mailbox, &[], message)
}
pub fn append_with_flags(
&mut self,
mailbox: &str,
flags: &[&str],
message: &[u8],
) -> ContainerResult<()> {
let tag = self.next_tag();
let flag_clause = if flags.is_empty() {
String::new()
} else {
format!(" ({})", flags.join(" "))
};
let cmd = format!(
"{tag} APPEND {}{flag_clause} {{{}}}\r\n",
quote(mailbox),
message.len(),
);
self.writer.write_all(cmd.as_bytes())?;
self.writer.flush()?;
let cont = self.read_line()?;
if !cont.starts_with('+') {
return Err(ContainerError::Protocol(format!(
"append expected continuation, got: {cont}"
)));
}
self.writer.write_all(message)?;
self.writer.write_all(b"\r\n")?;
self.writer.flush()?;
self.read_tagged(&tag)?;
Ok(())
}
pub fn list_all(&mut self) -> ContainerResult<Vec<String>> {
let tag = self.next_tag();
self.send(&format!(r#"{tag} LIST "" "*""#))?;
let untagged = self.read_tagged(&tag)?;
let mut names = Vec::new();
for line in untagged {
if let Some(rest) = line.strip_prefix("* LIST ")
&& let Some(name) = parse_list_name(rest)
{
names.push(name);
}
}
Ok(names)
}
pub fn select(&mut self, mailbox: &str) -> ContainerResult<usize> {
let tag = self.next_tag();
self.send(&format!("{tag} SELECT {}", quote(mailbox)))?;
let untagged = self.read_tagged(&tag)?;
for line in untagged {
if let Some(rest) = line.strip_prefix("* ")
&& let Some(num) = rest.strip_suffix(" EXISTS")
&& let Ok(n) = num.parse::<usize>()
{
return Ok(n);
}
}
Ok(0)
}
pub fn delete_and_expunge_first(&mut self, mailbox: &str) -> ContainerResult<()> {
self.select(mailbox)?;
let store_tag = self.next_tag();
self.send(&format!("{store_tag} STORE 1 +FLAGS (\\Deleted)"))?;
self.read_tagged(&store_tag)?;
let expunge_tag = self.next_tag();
self.send(&format!("{expunge_tag} EXPUNGE"))?;
self.read_tagged(&expunge_tag)?;
Ok(())
}
pub fn logout(&mut self) -> ContainerResult<()> {
let tag = self.next_tag();
self.send(&format!("{tag} LOGOUT"))?;
let mut buf = String::new();
let _ = self.reader.read_to_string(&mut buf);
Ok(())
}
}
pub fn full_path(specs: &[MailboxSpec], key: &str, separator: char) -> Option<String> {
let mut chain: Vec<&str> = Vec::new();
let mut cur = key;
loop {
let spec = specs.iter().find(|s| s.key == cur)?;
chain.push(spec.name);
match spec.parent {
Some(p) => cur = p,
None => break,
}
}
chain.reverse();
Some(chain.join(&separator.to_string()))
}
fn quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
if c == '\\' || c == '"' {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}
fn parse_list_name(line: &str) -> Option<String> {
let mut chars = line.chars();
let _ = chars.next()?;
let mut depth = 1;
let mut idx = 1;
for (i, c) in line[1..].char_indices() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
idx = i + 2;
break;
}
}
_ => {}
}
}
let rest = line[idx..].trim_start();
let mut parts = rest.splitn(2, ' ');
let _sep_token = parts.next()?;
let name_token = parts.next()?.trim();
if let Some(stripped) = name_token.strip_prefix('"') {
let end = stripped.find('"')?;
Some(stripped[..end].to_owned())
} else {
Some(name_token.to_owned())
}
}