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:
parent
4835df070e
commit
35c3c8657a
5 changed files with 279 additions and 4 deletions
|
|
@ -33,6 +33,9 @@ config = "0.13"
|
||||||
async-imap = "0.9"
|
async-imap = "0.9"
|
||||||
async-std = { version = "1.12", features = ["attributes"] }
|
async-std = { version = "1.12", features = ["attributes"] }
|
||||||
|
|
||||||
|
# TLS support for secure IMAP connections
|
||||||
|
async-native-tls = "0.5"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
|
|
|
||||||
98
rust/TLS_SUPPORT.md
Normal file
98
rust/TLS_SUPPORT.md
Normal 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.
|
||||||
26
rust/config-gmail-example.json
Normal file
26
rust/config-gmail-example.json
Normal 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
48
rust/config-tls-test.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
108
rust/src/imap.rs
108
rust/src/imap.rs
|
|
@ -8,10 +8,14 @@ use crate::schemas::MailDocument;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_imap::types::Fetch;
|
use async_imap::types::Fetch;
|
||||||
use async_imap::{Client, Session};
|
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::net::TcpStream;
|
||||||
use async_std::stream::StreamExt;
|
use async_std::stream::StreamExt;
|
||||||
|
use async_std::task::{Context, Poll};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::pin::Pin;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -26,7 +30,62 @@ pub enum ImapError {
|
||||||
Parsing(String),
|
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
|
/// IMAP client for mail operations
|
||||||
pub struct ImapClient {
|
pub struct ImapClient {
|
||||||
|
|
@ -63,9 +122,29 @@ impl ImapClient {
|
||||||
let tcp_stream = TcpStream::connect(&addr).await
|
let tcp_stream = TcpStream::connect(&addr).await
|
||||||
.map_err(|e| ImapError::Connection(format!("Failed to connect to {}: {}", addr, e)))?;
|
.map_err(|e| ImapError::Connection(format!("Failed to connect to {}: {}", addr, e)))?;
|
||||||
|
|
||||||
// For now, use unsecured connection (this should be made configurable)
|
// Determine if we should use TLS based on port
|
||||||
// In production, you'd want to use TLS
|
let imap_stream = if self.should_use_tls() {
|
||||||
let client = Client::new(tcp_stream);
|
// 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
|
// Perform IMAP login
|
||||||
let session = client
|
let session = client
|
||||||
|
|
@ -77,6 +156,27 @@ impl ImapClient {
|
||||||
Ok(())
|
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
|
/// List all mailboxes
|
||||||
pub async fn list_mailboxes(&mut self) -> Result<Vec<String>> {
|
pub async fn list_mailboxes(&mut self) -> Result<Vec<String>> {
|
||||||
let session = self.session.as_mut()
|
let session = self.session.as_mut()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue