refactor: replace rust-pop3-client with custom POP3 implementation

Replace third-party POP3 client with a custom implementation using
native-tls for direct TLS socket communication. This change ensures
email messages are retrieved as raw bytes without encoding conversions
that could corrupt email data.

Key improvements:
- Direct byte-level message retrieval preserves original email structure
- Proper handling of POP3 byte-stuffing (doubled leading dots)
- Eliminates dependency on rust-pop3-client which performed unwanted
  string conversions and line ending modifications
- Uses only native-tls for TLS connections (already a project dependency)
This commit is contained in:
2025-12-12 10:37:49 +01:00
parent e0b0c5e964
commit 113a72f1d6
3 changed files with 123 additions and 264 deletions
+114 -15
View File
@@ -1,43 +1,142 @@
use crate::config::settings::Pop3Config;
use rust_pop3_client::Pop3Connection;
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
use native_tls::TlsConnector;
pub struct Pop3Client {
connection: Pop3Connection,
stream: BufReader<native_tls::TlsStream<TcpStream>>,
}
impl Pop3Client {
pub fn new(config: &Pop3Config) -> Result<Self, Box<dyn std::error::Error>> {
let connection = Pop3Connection::new(&config.host, config.port)?;
Ok(Pop3Client { connection })
// Connect to POP3 server
let tcp_stream = TcpStream::connect((&config.host[..], config.port))?;
// Wrap with TLS
let connector = TlsConnector::new()?;
let tls_stream = connector.connect(&config.host, tcp_stream)?;
let stream = BufReader::new(tls_stream);
let mut client = Pop3Client { stream };
// Read greeting
client.read_response()?;
Ok(client)
}
pub fn login(&mut self, config: &Pop3Config) -> Result<(), Box<dyn std::error::Error>> {
self.connection.login(&config.username, &config.password)?;
// Send USER command
self.send_command(&format!("USER {}\r\n", config.username))?;
self.read_response()?;
// Send PASS command
self.send_command(&format!("PASS {}\r\n", config.password))?;
self.read_response()?;
Ok(())
}
pub fn list_messages(&mut self) -> Result<Vec<(u32, u32)>, Box<dyn std::error::Error>> {
let infos = self.connection.list()?;
let list = infos.into_iter().map(|info| (info.message_id, info.message_size)).collect();
Ok(list)
self.send_command("LIST\r\n")?;
let mut messages = Vec::new();
let mut line = String::new();
// Read first response line
self.stream.read_line(&mut line)?;
if !line.starts_with("+OK") {
return Err(format!("LIST failed: {}", line).into());
}
// Read message list
loop {
line.clear();
self.stream.read_line(&mut line)?;
if line.trim() == "." {
break;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let id: u32 = parts[0].parse()?;
let size: u32 = parts[1].parse()?;
messages.push((id, size));
}
}
Ok(messages)
}
pub fn retrieve_message(&mut self, msg_id: u32) -> Result<String, Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
self.connection.retrieve(msg_id, &mut buffer)?;
let message = String::from_utf8(buffer)?;
Ok(message)
self.send_command(&format!("RETR {}\r\n", msg_id))?;
let mut line = String::new();
// Read first response line
self.stream.read_line(&mut line)?;
if !line.starts_with("+OK") {
return Err(format!("RETR failed: {}", line).into());
}
// Read message content as raw bytes
let mut message_bytes = Vec::new();
let mut line_bytes = Vec::new();
loop {
line_bytes.clear();
let bytes_read = self.stream.read_until(b'\n', &mut line_bytes)?;
if bytes_read == 0 {
break;
}
// Check for termination (lone period)
if line_bytes == b".\r\n" || line_bytes == b".\n" {
break;
}
// Handle byte-stuffing (POP3 doubles leading dots)
if line_bytes.starts_with(b"..") {
message_bytes.extend_from_slice(&line_bytes[1..]);
} else {
message_bytes.extend_from_slice(&line_bytes);
}
}
// Convert to String using lossy conversion
// This preserves the structure while handling any encoding issues
Ok(String::from_utf8_lossy(&message_bytes).into_owned())
}
#[allow(dead_code)]
pub fn delete_message(&mut self, msg_id: u32) -> Result<(), Box<dyn std::error::Error>> {
self.connection.delete(msg_id)?;
self.send_command(&format!("DELE {}\r\n", msg_id))?;
self.read_response()?;
Ok(())
}
pub fn quit(&mut self) -> Result<(), Box<dyn std::error::Error>> {
// The rust-pop3-client doesn't seem to have an explicit quit method
// The connection should be closed when the object is dropped
self.send_command("QUIT\r\n")?;
self.read_response()?;
Ok(())
}
fn send_command(&mut self, command: &str) -> Result<(), Box<dyn std::error::Error>> {
self.stream.get_mut().write_all(command.as_bytes())?;
self.stream.get_mut().flush()?;
Ok(())
}
fn read_response(&mut self) -> Result<String, Box<dyn std::error::Error>> {
let mut line = String::new();
self.stream.read_line(&mut line)?;
if !line.starts_with("+OK") {
return Err(format!("POP3 error: {}", line).into());
}
Ok(line)
}
}