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
|
|
@ -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