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:
parent
d3d104ee71
commit
8764b44a05
9 changed files with 532 additions and 13 deletions
98
README.md
98
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/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
|
||||||
|
|
|
||||||
176
docs/ENVIRONMENT_VARIABLES.md
Normal file
176
docs/ENVIRONMENT_VARIABLES.md
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
11
docs/TODO.md
11
docs/TODO.md
|
|
@ -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**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue