diff --git a/README.md b/README.md index 8267464..092379c 100644 --- a/README.md +++ b/README.md @@ -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/ANALYSIS.md](docs/ANALYSIS.md)** - Technical analysis and current status - **[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/couchdb-schemas.md](docs/couchdb-schemas.md)** - Database schema documentation - **[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: -c, --config FILE Path to configuration file -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 ``` +### 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__USER` | Override IMAP username for source `` | `user@gmail.com` | +| `MAIL2COUCH_IMAP__PASSWORD` | Override IMAP password for source `` | `app_password` | + +**Name Normalization**: The `` 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 | 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 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 ### Security Considerations -- Use app passwords instead of account passwords -- Store configuration files with restricted permissions (600) -- Use HTTPS for CouchDB connections in production -- Consider encrypting sensitive configuration data +- **Use Environment Variables**: Store sensitive credentials in environment variables instead of configuration files +- **Use App Passwords**: Use app passwords instead of account passwords for IMAP authentication +- **File Permissions**: Store configuration files with restricted permissions (600) +- **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 - Review sync metadata documents for sync health diff --git a/docs/ENVIRONMENT_VARIABLES.md b/docs/ENVIRONMENT_VARIABLES.md new file mode 100644 index 0000000..256eb57 --- /dev/null +++ b/docs/ENVIRONMENT_VARIABLES.md @@ -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__USER` | Override IMAP username for source `` | `user@gmail.com` | +| `MAIL2COUCH_IMAP__PASSWORD` | Override IMAP password for source `` | `app_specific_password` | + +## Name Normalization + +The `` 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 \ No newline at end of file diff --git a/docs/IMPLEMENTATION_COMPARISON.md b/docs/IMPLEMENTATION_COMPARISON.md index 9dbc6f0..b416fa1 100644 --- a/docs/IMPLEMENTATION_COMPARISON.md +++ b/docs/IMPLEMENTATION_COMPARISON.md @@ -138,11 +138,14 @@ Both implementations have achieved **production readiness** with comprehensive t - ✅ **SystemD Integration**: Automated scheduling support - ✅ **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** -1. **Security**: Environment variable credential support -2. **Go Concurrency**: Optional parallel processing -3. **Progress Indicators**: Real-time progress reporting -4. **Interactive Setup**: Guided configuration wizard +1. **Go Concurrency**: Optional parallel processing +2. **Progress Indicators**: Real-time progress reporting +3. **Interactive Setup**: Guided configuration wizard ## Conclusion diff --git a/docs/README.md b/docs/README.md index ba9647b..cb587b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ This directory contains comprehensive documentation for the mail2couch project, - **[TODO.md](TODO.md)** - Development roadmap and outstanding tasks ### 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 - **[test-config-comparison.md](test-config-comparison.md)** - Configuration examples and testing scenarios diff --git a/docs/TODO.md b/docs/TODO.md index 1df605f..a16b50a 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -34,11 +34,14 @@ This document outlines the development roadmap for mail2couch, with both Go and ## 🚧 Current Development Priorities -### High Priority +### Recently Completed ✅ 1. **🔐 Enhanced Security Model** - - Environment variable credential support (`MAIL2COUCH_IMAP_PASSWORD`, etc.) - - Eliminate plaintext passwords from configuration files - - System keyring integration for credential storage + - ✅ Environment variable credential support (`MAIL2COUCH_IMAP_PASSWORD`, etc.) + - ✅ Eliminate plaintext passwords from configuration files + - ✅ Comprehensive documentation and help integration + - ❌ System keyring integration for credential storage (future enhancement) + +### High Priority ### Medium Priority 2. **🚀 Go Implementation Concurrency** diff --git a/go/config/config.go b/go/config/config.go index b7bf6ca..c3649e9 100644 --- a/go/config/config.go +++ b/go/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "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 } +// 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__USER: Override IMAP username for source named +// - MAIL2COUCH_IMAP__PASSWORD: Override IMAP password for source named +// +// The 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 func (ms *MailSource) IsSyncMode() bool { 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, "Options:\n") 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__USER Override IMAP username for source \n") + fmt.Fprintf(os.Stderr, " MAIL2COUCH_IMAP__PASSWORD Override IMAP password for source \n\n") + fmt.Fprintf(os.Stderr, " Where 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) } @@ -212,6 +290,38 @@ func FindConfigFile(args *CommandLineArgs) (string, error) { 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 func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) { configPath, err := FindConfigFile(args) @@ -228,5 +338,14 @@ func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) { if args.DryRun { 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 } diff --git a/rust/src/cli.rs b/rust/src/cli.rs index fea0afa..a572722 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -15,6 +15,23 @@ pub fn parse_command_line() -> CommandLineArgs { .version(env!("CARGO_PKG_VERSION")) .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.") + .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__USER Override IMAP username for source + MAIL2COUCH_IMAP__PASSWORD Override IMAP password for source + + Where 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") .short('c') .long("config") diff --git a/rust/src/config.rs b/rust/src/config.rs index 49d243a..b8ea2d5 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -4,6 +4,7 @@ //! file discovery, matching the behavior of the Go implementation. use serde::{Deserialize, Serialize}; +use std::env; use std::fs; use std::path::PathBuf; use thiserror::Error; @@ -131,6 +132,9 @@ impl Config { } } + // Apply environment variable overrides for sensitive credentials + config.apply_environment_overrides(); + Ok(config) } @@ -186,6 +190,109 @@ impl Config { let config = Self::load_from_path(config_path.to_str().unwrap())?; 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__USER: Override IMAP username for source named + /// - MAIL2COUCH_IMAP__PASSWORD: Override IMAP password for source named + /// + /// The 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::>() + .join("_"); + + cleaned } #[cfg(test)] diff --git a/rust/src/main.rs b/rust/src/main.rs index 8d4b2c6..37038f6 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -62,6 +62,9 @@ fn print_config_summary(config: &mail2couch::config::Config) { info!(" CouchDB: {}", config.couch_db.url); 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() { let status = if source.enabled { "enabled"