feat: add TLS support to Rust implementation

- Add async-native-tls dependency for secure IMAP connections
- Implement ImapStream enum supporting both TLS and plain connections
- Add automatic TLS detection based on port (993=TLS, 143=plain, 3143=test)
- Add comprehensive Read/Write trait implementations for stream wrapper
- Add debug logging for connection type verification
- Create example configurations for Gmail, Outlook, and other providers
- Add TLS_SUPPORT.md documentation with security guidelines
- Test with existing test environment and TLS detection logic
- Maintain backward compatibility with plain IMAP for testing

The Rust implementation now supports secure connections to production
email providers while maintaining compatibility with test environments.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-02 20:01:42 +02:00
commit 35c3c8657a
5 changed files with 279 additions and 4 deletions

View file

@ -8,10 +8,14 @@ use crate::schemas::MailDocument;
use anyhow::{anyhow, Result};
use async_imap::types::Fetch;
use async_imap::{Client, Session};
use async_native_tls::{TlsConnector, TlsStream};
use async_std::io::{Read, Write};
use async_std::net::TcpStream;
use async_std::stream::StreamExt;
use async_std::task::{Context, Poll};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::pin::Pin;
use thiserror::Error;
#[derive(Error, Debug)]
@ -26,7 +30,62 @@ pub enum ImapError {
Parsing(String),
}
pub type ImapSession = Session<TcpStream>;
/// Wrapper for both TLS and plain TCP streams
pub enum ImapStream {
Plain(TcpStream),
Tls(TlsStream<TcpStream>),
}
impl Read for ImapStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
match self.get_mut() {
ImapStream::Plain(stream) => Pin::new(stream).poll_read(cx, buf),
ImapStream::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}
impl Write for ImapStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
match self.get_mut() {
ImapStream::Plain(stream) => Pin::new(stream).poll_write(cx, buf),
ImapStream::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
match self.get_mut() {
ImapStream::Plain(stream) => Pin::new(stream).poll_flush(cx),
ImapStream::Tls(stream) => Pin::new(stream).poll_flush(cx),
}
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
match self.get_mut() {
ImapStream::Plain(stream) => Pin::new(stream).poll_close(cx),
ImapStream::Tls(stream) => Pin::new(stream).poll_close(cx),
}
}
}
impl std::fmt::Debug for ImapStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImapStream::Plain(_) => write!(f, "ImapStream::Plain(_)"),
ImapStream::Tls(_) => write!(f, "ImapStream::Tls(_)"),
}
}
}
pub type ImapSession = Session<ImapStream>;
/// IMAP client for mail operations
pub struct ImapClient {
@ -63,9 +122,29 @@ impl ImapClient {
let tcp_stream = TcpStream::connect(&addr).await
.map_err(|e| ImapError::Connection(format!("Failed to connect to {}: {}", addr, e)))?;
// For now, use unsecured connection (this should be made configurable)
// In production, you'd want to use TLS
let client = Client::new(tcp_stream);
// Determine if we should use TLS based on port
let imap_stream = if self.should_use_tls() {
// Use TLS for secure connection (typically port 993)
let tls_connector = TlsConnector::new();
let tls_stream = tls_connector.connect(&self.source.host, tcp_stream).await
.map_err(|e| ImapError::Connection(format!("TLS connection failed: {}", e)))?;
ImapStream::Tls(tls_stream)
} else {
// Use plain connection (typically port 143 or test environments)
ImapStream::Plain(tcp_stream)
};
// Log connection type for debugging
let connection_type = if self.should_use_tls() { "TLS" } else { "Plain" };
log::debug!(
"Connecting to {}:{} using {} connection",
self.source.host,
self.source.port,
connection_type
);
// Create IMAP client
let client = Client::new(imap_stream);
// Perform IMAP login
let session = client
@ -77,6 +156,27 @@ impl ImapClient {
Ok(())
}
/// Determine if TLS should be used based on port and configuration
fn should_use_tls(&self) -> bool {
// Standard IMAPS port (993) uses TLS
// Port 143 is typically plain IMAP
// Port 3143 is used in test environments (plain)
match self.source.port {
993 => true, // Standard IMAPS port
143 => false, // Standard IMAP port
3143 => false, // Test environment port
_ => {
// For other ports, default to TLS for security
// but log a warning
log::warn!(
"Unknown IMAP port {}, defaulting to TLS. Use port 143 for plain IMAP or 993 for TLS",
self.source.port
);
true
}
}
}
/// List all mailboxes
pub async fn list_mailboxes(&mut self) -> Result<Vec<String>> {
let session = self.session.as_mut()