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

@ -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"

98
rust/TLS_SUPPORT.md Normal file
View file

@ -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.

View file

@ -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"
}
}
]
}

48
rust/config-tls-test.json Normal file
View file

@ -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"]
}
}
]
}

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