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/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_<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
|
||||
|
||||
| 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
|
||||
|
|
|
|||
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
|
||||
- ✅ **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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
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
|
||||
|
||||
### 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**
|
||||
|
|
|
|||
|
|
@ -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_<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
|
||||
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_<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)
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_<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")
|
||||
.short('c')
|
||||
.long("config")
|
||||
|
|
|
|||
|
|
@ -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_<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)]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue