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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue