feat: implement comprehensive environment variable credential support

- Add environment variable overrides for sensitive credentials in both Go and Rust implementations
- Support MAIL2COUCH_COUCHDB_USER and MAIL2COUCH_COUCHDB_PASSWORD for CouchDB credentials
- Support MAIL2COUCH_IMAP_<NAME>_USER and MAIL2COUCH_IMAP_<NAME>_PASSWORD for IMAP credentials
- Implement automatic name normalization for mail source names to environment variable format
- Add runtime display of active environment variable overrides
- Enhance --help output in both implementations with comprehensive environment variable documentation
- Add detailed environment variable section to README with usage examples and security benefits
- Create comprehensive ENVIRONMENT_VARIABLES.md reference guide with SystemD, Docker, and CI/CD examples
- Update all documentation indices and cross-references
- Include security best practices and troubleshooting guidance
- Maintain full backward compatibility with existing configuration files

This enhancement addresses the high-priority security requirement to eliminate plaintext
passwords from configuration files while providing production-ready credential management
for both development and deployment scenarios.

🤖 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-07 15:09:34 +02:00
commit 8764b44a05
9 changed files with 532 additions and 13 deletions

View file

@ -59,6 +59,7 @@ Comprehensive documentation is available in the [`docs/`](docs/) directory:
- **[docs/README.md](docs/README.md)** - Documentation overview and quick start - **[docs/README.md](docs/README.md)** - Documentation overview and quick start
- **[docs/ANALYSIS.md](docs/ANALYSIS.md)** - Technical analysis and current status - **[docs/ANALYSIS.md](docs/ANALYSIS.md)** - Technical analysis and current status
- **[docs/IMPLEMENTATION_COMPARISON.md](docs/IMPLEMENTATION_COMPARISON.md)** - Go vs Rust comparison - **[docs/IMPLEMENTATION_COMPARISON.md](docs/IMPLEMENTATION_COMPARISON.md)** - Go vs Rust comparison
- **[docs/ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md)** - Environment variable credential overrides
- **[docs/FOLDER_PATTERNS.md](docs/FOLDER_PATTERNS.md)** - Folder filtering guide - **[docs/FOLDER_PATTERNS.md](docs/FOLDER_PATTERNS.md)** - Folder filtering guide
- **[docs/couchdb-schemas.md](docs/couchdb-schemas.md)** - Database schema documentation - **[docs/couchdb-schemas.md](docs/couchdb-schemas.md)** - Database schema documentation
- **[docs/TODO.md](docs/TODO.md)** - Development roadmap and future plans - **[docs/TODO.md](docs/TODO.md)** - Development roadmap and future plans
@ -139,9 +140,50 @@ mail2couch automatically searches for configuration files in this order:
Options: Options:
-c, --config FILE Path to configuration file -c, --config FILE Path to configuration file
-m, --max-messages N Limit messages processed per mailbox per run (0 = unlimited) -m, --max-messages N Limit messages processed per mailbox per run (0 = unlimited)
-n, --dry-run Show what would be done without making changes
-h, --help Show help message -h, --help Show help message
``` ```
### Environment Variables
mail2couch supports environment variable overrides for sensitive credentials, allowing you to keep passwords out of configuration files:
| Environment Variable | Purpose | Example |
|---------------------|---------|---------|
| `MAIL2COUCH_COUCHDB_USER` | Override CouchDB username | `admin` |
| `MAIL2COUCH_COUCHDB_PASSWORD` | Override CouchDB password | `secure_password` |
| `MAIL2COUCH_IMAP_<NAME>_USER` | Override IMAP username for source `<NAME>` | `user@gmail.com` |
| `MAIL2COUCH_IMAP_<NAME>_PASSWORD` | Override IMAP password for source `<NAME>` | `app_password` |
**Name Normalization**: The `<NAME>` part is the mail source name from your configuration converted to uppercase with non-alphanumeric characters replaced by underscores.
**Examples**:
- Source name `"Personal Gmail"``MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD`
- Source name `"Work Email"``MAIL2COUCH_IMAP_WORK_EMAIL_USER`
- Source name `"Self-Hosted Mail"``MAIL2COUCH_IMAP_SELF_HOSTED_MAIL_PASSWORD`
**Usage Examples**:
```bash
# Override CouchDB credentials
MAIL2COUCH_COUCHDB_PASSWORD=secret ./mail2couch
# Override IMAP password for a specific account
MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD=app-password ./mail2couch
# Use with systemd service (in service file)
Environment="MAIL2COUCH_COUCHDB_PASSWORD=secret"
Environment="MAIL2COUCH_IMAP_WORK_EMAIL_PASSWORD=app-pass"
# Combine with other options
MAIL2COUCH_COUCHDB_USER=backup_user ./mail2couch --dry-run --max-messages 100
```
**Security Benefits**:
- ✅ Keep sensitive credentials out of configuration files
- ✅ Use different credentials per environment (dev/staging/prod)
- ✅ Integrate with CI/CD systems and secret management
- ✅ Maintain backward compatibility with existing configurations
### Folder Pattern Examples ### Folder Pattern Examples
| Pattern | Description | Matches | | Pattern | Description | Matches |
@ -277,6 +319,47 @@ Basic setup for a single Gmail account:
} }
``` ```
### Secure Configuration with Environment Variables
Configuration using placeholder values with sensitive credentials provided via environment variables:
```json
{
"couchDb": {
"url": "http://localhost:5984",
"user": "placeholder",
"password": "placeholder"
},
"mailSources": [
{
"name": "Personal Gmail",
"enabled": true,
"protocol": "imap",
"host": "imap.gmail.com",
"port": 993,
"user": "placeholder",
"password": "placeholder",
"mode": "archive",
"folderFilter": {
"include": ["INBOX", "Sent"],
"exclude": []
}
}
]
}
```
Run with environment variables:
```bash
# Set credentials via environment variables
export MAIL2COUCH_COUCHDB_USER="admin"
export MAIL2COUCH_COUCHDB_PASSWORD="secure_couchdb_password"
export MAIL2COUCH_IMAP_PERSONAL_GMAIL_USER="your-email@gmail.com"
export MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD="your-app-password"
# Run mail2couch (credentials will override config placeholders)
./mail2couch
```
### Advanced Multi-Account Configuration ### Advanced Multi-Account Configuration
Complex setup with multiple accounts, filtering, and different sync modes: Complex setup with multiple accounts, filtering, and different sync modes:
@ -391,10 +474,17 @@ Complex setup with multiple accounts, filtering, and different sync modes:
## Production Deployment ## Production Deployment
### Security Considerations ### Security Considerations
- Use app passwords instead of account passwords - **Use Environment Variables**: Store sensitive credentials in environment variables instead of configuration files
- Store configuration files with restricted permissions (600) - **Use App Passwords**: Use app passwords instead of account passwords for IMAP authentication
- Use HTTPS for CouchDB connections in production - **File Permissions**: Store configuration files with restricted permissions (600)
- Consider encrypting sensitive configuration data - **Secure Connections**: Use HTTPS for CouchDB connections in production
- **SystemD Integration**: Use environment variables in systemd service files:
```ini
[Service]
Environment="MAIL2COUCH_COUCHDB_PASSWORD=secure_password"
Environment="MAIL2COUCH_IMAP_GMAIL_PASSWORD=app_password"
ExecStart=/usr/local/bin/mail2couch
```
### Monitoring and Maintenance ### Monitoring and Maintenance
- Review sync metadata documents for sync health - Review sync metadata documents for sync health

View file

@ -0,0 +1,176 @@
# Environment Variables Reference
mail2couch supports environment variable overrides for sensitive credentials, allowing you to keep passwords and usernames out of configuration files while maintaining security and flexibility.
## Overview
Environment variables take precedence over values specified in configuration files, enabling:
- ✅ Secure credential management without plaintext passwords in config files
- ✅ Different credentials per environment (development, staging, production)
- ✅ Integration with CI/CD systems and secret management tools
- ✅ SystemD service configuration with secure credential injection
## Supported Environment Variables
| Variable | Purpose | Example Value |
|----------|---------|---------------|
| `MAIL2COUCH_COUCHDB_USER` | Override CouchDB username | `admin` |
| `MAIL2COUCH_COUCHDB_PASSWORD` | Override CouchDB password | `secure_password123` |
| `MAIL2COUCH_IMAP_<NAME>_USER` | Override IMAP username for source `<NAME>` | `user@gmail.com` |
| `MAIL2COUCH_IMAP_<NAME>_PASSWORD` | Override IMAP password for source `<NAME>` | `app_specific_password` |
## Name Normalization
The `<NAME>` portion of IMAP environment variables corresponds to your mail source name from the configuration file, normalized according to these rules:
1. **Convert to uppercase**: `Personal Gmail``PERSONAL GMAIL`
2. **Replace non-alphanumeric characters with underscores**: `PERSONAL GMAIL``PERSONAL_GMAIL`
3. **Clean up consecutive underscores**: `WORK__EMAIL``WORK_EMAIL`
### Examples
| Configuration Source Name | Environment Variable Prefix |
|---------------------------|------------------------------|
| `"Personal Gmail"` | `MAIL2COUCH_IMAP_PERSONAL_GMAIL_` |
| `"Work Email"` | `MAIL2COUCH_IMAP_WORK_EMAIL_` |
| `"Self-Hosted Mail"` | `MAIL2COUCH_IMAP_SELF_HOSTED_MAIL_` |
| `"Company.com Account"` | `MAIL2COUCH_IMAP_COMPANY_COM_ACCOUNT_` |
## Usage Examples
### Basic Override
```bash
# Override CouchDB password
export MAIL2COUCH_COUCHDB_PASSWORD="secure_password"
./mail2couch
# Override specific IMAP account credentials
export MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD="app_password_123"
./mail2couch --config my-config.json
```
### Multiple Account Setup
```bash
# Set credentials for multiple accounts
export MAIL2COUCH_COUCHDB_USER="backup_user"
export MAIL2COUCH_COUCHDB_PASSWORD="backup_password"
export MAIL2COUCH_IMAP_WORK_EMAIL_USER="work@company.com"
export MAIL2COUCH_IMAP_WORK_EMAIL_PASSWORD="work_app_password"
export MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD="gmail_app_password"
./mail2couch --max-messages 1000
```
### SystemD Service Configuration
```ini
[Unit]
Description=Mail2Couch Email Backup
After=network.target
[Service]
Type=oneshot
User=mail2couch
Environment="MAIL2COUCH_COUCHDB_PASSWORD=secure_password"
Environment="MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD=gmail_app_pass"
Environment="MAIL2COUCH_IMAP_WORK_EMAIL_PASSWORD=work_app_pass"
ExecStart=/usr/local/bin/mail2couch
WorkingDirectory=/home/mail2couch
[Install]
WantedBy=multi-user.target
```
### Docker/Container Usage
```bash
# Docker run with environment variables
docker run -e MAIL2COUCH_COUCHDB_PASSWORD=secret \
-e MAIL2COUCH_IMAP_GMAIL_PASSWORD=app_pass \
-v /path/to/config.json:/config.json \
mail2couch --config /config.json
# Docker Compose
version: '3.8'
services:
mail2couch:
image: mail2couch:latest
environment:
- MAIL2COUCH_COUCHDB_PASSWORD=${COUCHDB_PASSWORD}
- MAIL2COUCH_IMAP_GMAIL_PASSWORD=${GMAIL_APP_PASSWORD}
volumes:
- ./config.json:/config.json
```
### CI/CD Integration
```yaml
# GitHub Actions example
- name: Run Mail2Couch Backup
run: ./mail2couch --dry-run
env:
MAIL2COUCH_COUCHDB_PASSWORD: ${{ secrets.COUCHDB_PASSWORD }}
MAIL2COUCH_IMAP_GMAIL_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }}
```
## Configuration File Template
When using environment variables, you can use placeholder values in your configuration file:
```json
{
"couchDb": {
"url": "https://couchdb.example.com:5984",
"user": "placeholder_will_be_overridden",
"password": "placeholder_will_be_overridden"
},
"mailSources": [
{
"name": "Personal Gmail",
"enabled": true,
"protocol": "imap",
"host": "imap.gmail.com",
"port": 993,
"user": "placeholder_will_be_overridden",
"password": "placeholder_will_be_overridden",
"mode": "archive"
}
]
}
```
## Verification
mail2couch will show which environment variable overrides are active during startup:
```
Using configuration file: config.json
Active environment variable overrides: MAIL2COUCH_COUCHDB_PASSWORD, MAIL2COUCH_IMAP_PERSONAL_GMAIL_PASSWORD
```
This helps you verify that your environment variables are being detected and applied correctly.
## Security Best Practices
1. **Never commit real credentials to version control** - Use placeholder values in config files
2. **Use app-specific passwords** - For Gmail, Outlook, and other providers that support them
3. **Restrict environment variable access** - Ensure only authorized users/processes can read the environment variables
4. **Rotate credentials regularly** - Update both app passwords and environment variable values periodically
5. **Use secret management systems** - In production, integrate with tools like HashiCorp Vault, AWS Secrets Manager, etc.
## Troubleshooting
### Environment Variables Not Working
- Check variable names match exactly (case-sensitive)
- Verify the source name normalization (see examples above)
- Ensure variables are exported in your shell session
- Check for typos in variable names
### SystemD Service Issues
- Verify Environment lines in service file are correct
- Check service status: `systemctl status mail2couch`
- View logs: `journalctl -u mail2couch -f`
- Test manually first: `sudo -u mail2couch mail2couch --dry-run`
### Verification Steps
1. Run with `--dry-run` to test without making changes
2. Check startup output for "Active environment variable overrides"
3. Verify authentication errors are resolved
4. Test with minimal configuration first

View file

@ -138,11 +138,14 @@ Both implementations have achieved **production readiness** with comprehensive t
- ✅ **SystemD Integration**: Automated scheduling support - ✅ **SystemD Integration**: Automated scheduling support
- ✅ **Build System**: Unified justfile for both implementations - ✅ **Build System**: Unified justfile for both implementations
### **Recently Completed Enhancements**
1. **✅ Security**: Environment variable credential support - both implementations support full credential override via environment variables
2. **✅ Documentation**: Comprehensive help text and README documentation for all security features
### **Future Enhancement Priorities** ### **Future Enhancement Priorities**
1. **Security**: Environment variable credential support 1. **Go Concurrency**: Optional parallel processing
2. **Go Concurrency**: Optional parallel processing 2. **Progress Indicators**: Real-time progress reporting
3. **Progress Indicators**: Real-time progress reporting 3. **Interactive Setup**: Guided configuration wizard
4. **Interactive Setup**: Guided configuration wizard
## Conclusion ## Conclusion

View file

@ -11,6 +11,7 @@ This directory contains comprehensive documentation for the mail2couch project,
- **[TODO.md](TODO.md)** - Development roadmap and outstanding tasks - **[TODO.md](TODO.md)** - Development roadmap and outstanding tasks
### Configuration & Setup ### Configuration & Setup
- **[ENVIRONMENT_VARIABLES.md](ENVIRONMENT_VARIABLES.md)** - Complete guide to environment variable credential overrides
- **[FOLDER_PATTERNS.md](FOLDER_PATTERNS.md)** - Guide to folder filtering patterns and wildcards - **[FOLDER_PATTERNS.md](FOLDER_PATTERNS.md)** - Guide to folder filtering patterns and wildcards
- **[test-config-comparison.md](test-config-comparison.md)** - Configuration examples and testing scenarios - **[test-config-comparison.md](test-config-comparison.md)** - Configuration examples and testing scenarios

View file

@ -34,11 +34,14 @@ This document outlines the development roadmap for mail2couch, with both Go and
## 🚧 Current Development Priorities ## 🚧 Current Development Priorities
### High Priority ### Recently Completed ✅
1. **🔐 Enhanced Security Model** 1. **🔐 Enhanced Security Model**
- Environment variable credential support (`MAIL2COUCH_IMAP_PASSWORD`, etc.) - ✅ Environment variable credential support (`MAIL2COUCH_IMAP_PASSWORD`, etc.)
- Eliminate plaintext passwords from configuration files - ✅ Eliminate plaintext passwords from configuration files
- System keyring integration for credential storage - ✅ Comprehensive documentation and help integration
- ❌ System keyring integration for credential storage (future enhancement)
### High Priority
### Medium Priority ### Medium Priority
2. **🚀 Go Implementation Concurrency** 2. **🚀 Go Implementation Concurrency**

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@ -75,9 +76,71 @@ func LoadConfig(path string) (*Config, error) {
} }
} }
// Apply environment variable overrides for sensitive credentials
applyEnvironmentOverrides(&config)
return &config, nil return &config, nil
} }
// applyEnvironmentOverrides applies environment variable overrides for sensitive credentials
// This allows users to keep credentials out of config files while maintaining security
//
// Environment variable patterns:
// - MAIL2COUCH_COUCHDB_USER: Override CouchDB username
// - MAIL2COUCH_COUCHDB_PASSWORD: Override CouchDB password
// - MAIL2COUCH_IMAP_<NAME>_USER: Override IMAP username for source named <NAME>
// - MAIL2COUCH_IMAP_<NAME>_PASSWORD: Override IMAP password for source named <NAME>
//
// The <NAME> part is the mail source name converted to uppercase with non-alphanumeric characters replaced with underscores
func applyEnvironmentOverrides(config *Config) {
// CouchDB credential overrides
if user := os.Getenv("MAIL2COUCH_COUCHDB_USER"); user != "" {
config.CouchDb.User = user
}
if password := os.Getenv("MAIL2COUCH_COUCHDB_PASSWORD"); password != "" {
config.CouchDb.Password = password
}
// IMAP credential overrides for each mail source
for i := range config.MailSources {
source := &config.MailSources[i]
// Convert source name to environment variable suffix
// Replace non-alphanumeric characters with underscores and uppercase
envSuffix := normalizeNameForEnvVar(source.Name)
userEnvVar := fmt.Sprintf("MAIL2COUCH_IMAP_%s_USER", envSuffix)
if user := os.Getenv(userEnvVar); user != "" {
source.User = user
}
passwordEnvVar := fmt.Sprintf("MAIL2COUCH_IMAP_%s_PASSWORD", envSuffix)
if password := os.Getenv(passwordEnvVar); password != "" {
source.Password = password
}
}
}
// normalizeNameForEnvVar converts a source name to a valid environment variable suffix
// Example: "Personal Gmail" -> "PERSONAL_GMAIL"
func normalizeNameForEnvVar(name string) string {
result := strings.ToUpper(name)
// Replace non-alphanumeric characters with underscores
var normalized strings.Builder
for _, char := range result {
if (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') {
normalized.WriteRune(char)
} else {
normalized.WriteRune('_')
}
}
// Clean up multiple consecutive underscores and trim
cleaned := strings.Trim(strings.ReplaceAll(normalized.String(), "__", "_"), "_")
return cleaned
}
// IsSyncMode returns true if the mail source is in sync mode // IsSyncMode returns true if the mail source is in sync mode
func (ms *MailSource) IsSyncMode() bool { func (ms *MailSource) IsSyncMode() bool {
return ms.Mode == "sync" return ms.Mode == "sync"
@ -116,6 +179,21 @@ func ParseCommandLine() *CommandLineArgs {
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Options:\n") fmt.Fprintf(os.Stderr, "Options:\n")
pflag.PrintDefaults() pflag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nEnvironment Variables:\n")
fmt.Fprintf(os.Stderr, " Override sensitive credentials to avoid storing them in config files:\n\n")
fmt.Fprintf(os.Stderr, " MAIL2COUCH_COUCHDB_USER Override CouchDB username\n")
fmt.Fprintf(os.Stderr, " MAIL2COUCH_COUCHDB_PASSWORD Override CouchDB password\n")
fmt.Fprintf(os.Stderr, " MAIL2COUCH_IMAP_<NAME>_USER Override IMAP username for source <NAME>\n")
fmt.Fprintf(os.Stderr, " MAIL2COUCH_IMAP_<NAME>_PASSWORD Override IMAP password for source <NAME>\n\n")
fmt.Fprintf(os.Stderr, " Where <NAME> is the mail source name from config.json converted to\n")
fmt.Fprintf(os.Stderr, " uppercase with non-alphanumeric characters replaced by underscores.\n")
fmt.Fprintf(os.Stderr, " Example: \"Personal Gmail\" -> \"PERSONAL_GMAIL\"\n\n")
fmt.Fprintf(os.Stderr, "Examples:\n")
fmt.Fprintf(os.Stderr, " %s --config /path/to/config.json\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s --max-messages 100 --dry-run\n", os.Args[0])
fmt.Fprintf(os.Stderr, " MAIL2COUCH_COUCHDB_PASSWORD=secret %s\n", os.Args[0])
fmt.Fprintf(os.Stderr, " MAIL2COUCH_IMAP_WORK_EMAIL_PASSWORD=app-pass %s\n", os.Args[0])
os.Stderr.Sync()
os.Exit(0) os.Exit(0)
} }
@ -212,6 +290,38 @@ func FindConfigFile(args *CommandLineArgs) (string, error) {
return "", fmt.Errorf("no configuration file found. Searched locations: %v", candidates) return "", fmt.Errorf("no configuration file found. Searched locations: %v", candidates)
} }
// showEnvironmentOverrides displays which environment variable overrides are active
func showEnvironmentOverrides(config *Config) {
var overrides []string
// Check CouchDB overrides
if os.Getenv("MAIL2COUCH_COUCHDB_USER") != "" {
overrides = append(overrides, "MAIL2COUCH_COUCHDB_USER")
}
if os.Getenv("MAIL2COUCH_COUCHDB_PASSWORD") != "" {
overrides = append(overrides, "MAIL2COUCH_COUCHDB_PASSWORD")
}
// Check IMAP overrides for each source
for _, source := range config.MailSources {
envSuffix := normalizeNameForEnvVar(source.Name)
userEnvVar := fmt.Sprintf("MAIL2COUCH_IMAP_%s_USER", envSuffix)
if os.Getenv(userEnvVar) != "" {
overrides = append(overrides, userEnvVar)
}
passwordEnvVar := fmt.Sprintf("MAIL2COUCH_IMAP_%s_PASSWORD", envSuffix)
if os.Getenv(passwordEnvVar) != "" {
overrides = append(overrides, passwordEnvVar)
}
}
if len(overrides) > 0 {
fmt.Printf("Active environment variable overrides: %s\n", strings.Join(overrides, ", "))
}
}
// LoadConfigWithDiscovery loads configuration using automatic file discovery // LoadConfigWithDiscovery loads configuration using automatic file discovery
func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) { func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) {
configPath, err := FindConfigFile(args) configPath, err := FindConfigFile(args)
@ -228,5 +338,14 @@ func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) {
if args.DryRun { if args.DryRun {
fmt.Printf("DRY-RUN MODE: No changes will be made to CouchDB\n") fmt.Printf("DRY-RUN MODE: No changes will be made to CouchDB\n")
} }
return LoadConfig(configPath)
config, err := LoadConfig(configPath)
if err != nil {
return nil, err
}
// Show which environment variable overrides are active
showEnvironmentOverrides(config)
return config, nil
} }

View file

@ -15,6 +15,23 @@ pub fn parse_command_line() -> CommandLineArgs {
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about("Email backup utility for CouchDB") .about("Email backup utility for CouchDB")
.long_about("A powerful email backup utility that synchronizes mail from IMAP accounts to CouchDB databases with intelligent incremental sync, comprehensive filtering, and native attachment support.") .long_about("A powerful email backup utility that synchronizes mail from IMAP accounts to CouchDB databases with intelligent incremental sync, comprehensive filtering, and native attachment support.")
.after_help(r#"Environment Variables:
Override sensitive credentials to avoid storing them in config files:
MAIL2COUCH_COUCHDB_USER Override CouchDB username
MAIL2COUCH_COUCHDB_PASSWORD Override CouchDB password
MAIL2COUCH_IMAP_<NAME>_USER Override IMAP username for source <NAME>
MAIL2COUCH_IMAP_<NAME>_PASSWORD Override IMAP password for source <NAME>
Where <NAME> is the mail source name from config.json converted to
uppercase with non-alphanumeric characters replaced by underscores.
Example: "Personal Gmail" -> "PERSONAL_GMAIL"
Examples:
mail2couch --config /path/to/config.json
mail2couch --max-messages 100 --dry-run
MAIL2COUCH_COUCHDB_PASSWORD=secret mail2couch
MAIL2COUCH_IMAP_WORK_EMAIL_PASSWORD=app-pass mail2couch"#)
.arg(Arg::new("config") .arg(Arg::new("config")
.short('c') .short('c')
.long("config") .long("config")

View file

@ -4,6 +4,7 @@
//! file discovery, matching the behavior of the Go implementation. //! file discovery, matching the behavior of the Go implementation.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::env;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
@ -131,6 +132,9 @@ impl Config {
} }
} }
// Apply environment variable overrides for sensitive credentials
config.apply_environment_overrides();
Ok(config) Ok(config)
} }
@ -186,6 +190,109 @@ impl Config {
let config = Self::load_from_path(config_path.to_str().unwrap())?; let config = Self::load_from_path(config_path.to_str().unwrap())?;
Ok((config, config_path)) Ok((config, config_path))
} }
/// Apply environment variable overrides for sensitive credentials
/// This allows users to keep credentials out of config files while maintaining security
///
/// Environment variable patterns:
/// - MAIL2COUCH_COUCHDB_USER: Override CouchDB username
/// - MAIL2COUCH_COUCHDB_PASSWORD: Override CouchDB password
/// - MAIL2COUCH_IMAP_<NAME>_USER: Override IMAP username for source named <NAME>
/// - MAIL2COUCH_IMAP_<NAME>_PASSWORD: Override IMAP password for source named <NAME>
///
/// The <NAME> part is the mail source name converted to uppercase with non-alphanumeric characters replaced with underscores
fn apply_environment_overrides(&mut self) {
// CouchDB credential overrides
if let Ok(user) = env::var("MAIL2COUCH_COUCHDB_USER") {
if !user.is_empty() {
self.couch_db.user = user;
}
}
if let Ok(password) = env::var("MAIL2COUCH_COUCHDB_PASSWORD") {
if !password.is_empty() {
self.couch_db.password = password;
}
}
// IMAP credential overrides for each mail source
for source in &mut self.mail_sources {
// Convert source name to environment variable suffix
let env_suffix = normalize_name_for_env_var(&source.name);
let user_env_var = format!("MAIL2COUCH_IMAP_{}_USER", env_suffix);
if let Ok(user) = env::var(&user_env_var) {
if !user.is_empty() {
source.user = user;
}
}
let password_env_var = format!("MAIL2COUCH_IMAP_{}_PASSWORD", env_suffix);
if let Ok(password) = env::var(&password_env_var) {
if !password.is_empty() {
source.password = password;
}
}
}
}
/// Show which environment variable overrides are active
pub fn show_environment_overrides(&self) {
let mut overrides = Vec::new();
// Check CouchDB overrides
if env::var("MAIL2COUCH_COUCHDB_USER").is_ok() {
overrides.push("MAIL2COUCH_COUCHDB_USER");
}
if env::var("MAIL2COUCH_COUCHDB_PASSWORD").is_ok() {
overrides.push("MAIL2COUCH_COUCHDB_PASSWORD");
}
// Check IMAP overrides for each source
for source in &self.mail_sources {
let env_suffix = normalize_name_for_env_var(&source.name);
let user_env_var = format!("MAIL2COUCH_IMAP_{}_USER", env_suffix);
if env::var(&user_env_var).is_ok() {
overrides.push(Box::leak(user_env_var.into_boxed_str()));
}
let password_env_var = format!("MAIL2COUCH_IMAP_{}_PASSWORD", env_suffix);
if env::var(&password_env_var).is_ok() {
overrides.push(Box::leak(password_env_var.into_boxed_str()));
}
}
if !overrides.is_empty() {
println!("Active environment variable overrides: {}", overrides.join(", "));
}
}
}
/// Normalize a source name to a valid environment variable suffix
/// Example: "Personal Gmail" -> "PERSONAL_GMAIL"
fn normalize_name_for_env_var(name: &str) -> String {
let upper_name = name.to_uppercase();
// Replace non-alphanumeric characters with underscores
let normalized: String = upper_name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c
} else {
'_'
}
})
.collect();
// Clean up multiple consecutive underscores and trim
let cleaned = normalized
.split('_')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("_");
cleaned
} }
#[cfg(test)] #[cfg(test)]

View file

@ -62,6 +62,9 @@ fn print_config_summary(config: &mail2couch::config::Config) {
info!(" CouchDB: {}", config.couch_db.url); info!(" CouchDB: {}", config.couch_db.url);
info!(" Mail sources: {}", config.mail_sources.len()); info!(" Mail sources: {}", config.mail_sources.len());
// Show which environment variable overrides are active
config.show_environment_overrides();
for (i, source) in config.mail_sources.iter().enumerate() { for (i, source) in config.mail_sources.iter().enumerate() {
let status = if source.enabled { let status = if source.enabled {
"enabled" "enabled"