diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7d2819a..fbdd2a4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -33,6 +33,9 @@ config = "0.13" async-imap = "0.9" async-std = { version = "1.12", features = ["attributes"] } +# TLS support for secure IMAP connections +async-native-tls = "0.5" + # Logging log = "0.4" env_logger = "0.10" diff --git a/rust/TLS_SUPPORT.md b/rust/TLS_SUPPORT.md new file mode 100644 index 0000000..f488d11 --- /dev/null +++ b/rust/TLS_SUPPORT.md @@ -0,0 +1,98 @@ +# TLS Support in mail2couch Rust Implementation + +The Rust implementation of mail2couch now includes full TLS support for secure IMAP connections. + +## Automatic TLS Detection + +The client automatically determines whether to use TLS based on the configured port: + +- **Port 993** (IMAPS): Uses TLS encryption (default for Gmail, Outlook, etc.) +- **Port 143** (IMAP): Uses plain text connection (insecure, typically for testing) +- **Port 3143**: Uses plain text (test environment default) +- **Other ports**: Defaults to TLS with a warning message + +## Example Configurations + +### Gmail with TLS (Recommended) +```json +{ + "name": "Personal Gmail", + "host": "imap.gmail.com", + "port": 993, + "user": "your-email@gmail.com", + "password": "your-app-password" +} +``` + +### Outlook with TLS +```json +{ + "name": "Work Outlook", + "host": "outlook.office365.com", + "port": 993, + "user": "you@company.com", + "password": "your-app-password" +} +``` + +### Test Environment (Plain) +```json +{ + "name": "Test Server", + "host": "localhost", + "port": 3143, + "user": "testuser", + "password": "testpass" +} +``` + +## Security Notes + +1. **Always use port 993** for production email providers +2. **Never use port 143** with real email accounts (credentials sent in plain text) +3. **Use app passwords** instead of account passwords for Gmail/Outlook +4. **Port 3143** is only for local testing environments + +## Provider-Specific Settings + +### Gmail +- Host: `imap.gmail.com` +- Port: `993` (TLS) +- Requires app password (not regular password) +- Enable 2FA and generate app password in Google Account settings + +### Microsoft Outlook/Office 365 +- Host: `outlook.office365.com` +- Port: `993` (TLS) +- May require app password depending on organization settings + +### Yahoo Mail +- Host: `imap.mail.yahoo.com` +- Port: `993` (TLS) +- Requires app password + +## Testing TLS Functionality + +1. **Test with local environment**: Port 3143 (plain) + ```bash + ./mail2couch -c config-test.json + ``` + +2. **Test with Gmail**: Port 993 (TLS) + ```bash + ./mail2couch -c config-gmail.json + ``` + +3. **Verify TLS detection**: Check logs for connection type + - TLS connections will show successful handshake + - Plain connections will connect directly + +## Implementation Details + +The TLS support is implemented using: +- `async-native-tls` for TLS connections +- `async-std` for plain TCP connections +- Custom `ImapStream` enum that wraps both connection types +- Automatic port-based detection logic + +This ensures compatibility with both secure production environments and insecure test setups. \ No newline at end of file diff --git a/rust/config-gmail-example.json b/rust/config-gmail-example.json new file mode 100644 index 0000000..dfbc3c9 --- /dev/null +++ b/rust/config-gmail-example.json @@ -0,0 +1,26 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "Personal Gmail", + "enabled": true, + "protocol": "imap", + "host": "imap.gmail.com", + "port": 993, + "user": "your-email@gmail.com", + "password": "your-app-password", + "mode": "archive", + "folderFilter": { + "include": ["INBOX", "[Gmail]/Sent Mail"], + "exclude": ["[Gmail]/Trash", "[Gmail]/Spam"] + }, + "messageFilter": { + "since": "2024-01-01" + } + } + ] +} \ No newline at end of file diff --git a/rust/config-tls-test.json b/rust/config-tls-test.json new file mode 100644 index 0000000..0248097 --- /dev/null +++ b/rust/config-tls-test.json @@ -0,0 +1,48 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "TLS Test Port 993", + "enabled": true, + "protocol": "imap", + "host": "imap.gmail.com", + "port": 993, + "user": "test@example.com", + "password": "dummy", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"] + } + }, + { + "name": "Plain Test Port 143", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 143, + "user": "test", + "password": "dummy", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"] + } + }, + { + "name": "Unknown Port Test", + "enabled": true, + "protocol": "imap", + "host": "example.com", + "port": 9999, + "user": "test", + "password": "dummy", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"] + } + } + ] +} \ No newline at end of file diff --git a/rust/src/imap.rs b/rust/src/imap.rs index 3e15fd1..f5680dd 100644 --- a/rust/src/imap.rs +++ b/rust/src/imap.rs @@ -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; +/// Wrapper for both TLS and plain TCP streams +pub enum ImapStream { + Plain(TcpStream), + Tls(TlsStream), +} + +impl Read for ImapStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + 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> { + 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> { + 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> { + 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; /// 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> { let session = self.session.as_mut()