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:29:00 +01:00
parent e0b0c5e964
commit 113a72f1d6
3 changed files with 123 additions and 264 deletions

255
Cargo.lock generated
View File

@@ -56,7 +56,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -88,12 +88,6 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -222,7 +216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -252,17 +246,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -311,7 +294,7 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c617c55def8c42129e0dd503f11d7ee39d73f5c7e01eff55768b3879ff1d107d"
dependencies = [
"base64 0.13.1",
"base64",
"bufstream",
"chrono",
"imap-proto 0.10.2",
@@ -566,35 +549,6 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted 0.7.1",
"web-sys",
"winapi",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted 0.9.0",
"windows-sys 0.52.0",
]
[[package]]
name = "rs_pop_imap_importer"
version = "0.5.0"
@@ -604,17 +558,6 @@ dependencies = [
"imap",
"imap-proto 0.16.6",
"native-tls",
"rust-pop3-client",
]
[[package]]
name = "rust-pop3-client"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e42fdc52a54878a9331d2b1cb2521092f43432f1fafab16f71337a886fb38"
dependencies = [
"rustls",
"rustls-native-certs",
]
[[package]]
@@ -627,40 +570,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
dependencies = [
"log",
"ring 0.16.20",
"sct",
"webpki",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
"windows-sys",
]
[[package]]
@@ -681,17 +591,7 @@ version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring 0.17.14",
"untrusted 0.9.0",
"windows-sys",
]
[[package]]
@@ -723,12 +623,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -759,10 +653,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -771,18 +665,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -801,12 +683,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
@@ -861,48 +737,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
dependencies = [
"ring 0.17.14",
"untrusted 0.9.0",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -962,15 +796,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -980,70 +805,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen"
version = "0.46.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "rs_pop_imap_importer"
version = "0.5.0"
version = "0.6.0"
edition = "2024"
[lib]
@@ -21,4 +21,3 @@ dotenvy = "0.15.7"
imap = "2.4.1"
imap-proto = "0.16.6"
native-tls = "0.2.14"
rust-pop3-client = "0.2.2"

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)
}
}