Consolidate all secrets into config.yaml — remove .env entirely
All configuration (including secrets) now lives in a single file: bin/config.yaml. The separate .env file has been eliminated. Changes: - config.go: Added SMTPSettings struct + AI.APIKey to Config; removed godotenv import, Environment struct, and all os.Getenv() calls - config.yaml: Added smtp section (host/port/username/password) and ai.api_key field with placeholder values - main.go: Reads SMTP and API key from cfg instead of env - smtp.go: Changed Port field from string to int - otp_test.go: Updated Port values to int - .env.example: Deleted (all config is in config.yaml) - .gitignore: Removed .env.example; kept .env for safety - go.mod/go.sum: Removed github.com/joho/godotenv dependency - install.sh: No longer creates .env or uses EnvironmentFile; warns about placeholder values in config.yaml instead
This commit is contained in:
parent
54dd30a2d6
commit
01a58156a1
11 changed files with 94 additions and 160 deletions
25
.env.example
25
.env.example
|
|
@ -1,25 +0,0 @@
|
||||||
# ───────────────────────────────────────────────────────
|
|
||||||
# inBOXER – Environment Configuration
|
|
||||||
# ───────────────────────────────────────────────────────
|
|
||||||
# Copy this file to .env and fill in your credentials:
|
|
||||||
# cp .env.example .env
|
|
||||||
# nano .env
|
|
||||||
#
|
|
||||||
# The application also reads these variables from the
|
|
||||||
# systemd EnvironmentFile (if deployed via install.sh).
|
|
||||||
# ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# DeepSeek API key – for AI-based email classification
|
|
||||||
# Sign up at https://platform.deepseek.com/ to get one.
|
|
||||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
|
||||||
|
|
||||||
# SMTP credentials – used to send one-time passwords (OTP)
|
|
||||||
# for user login. Supports STARTTLS on port 587.
|
|
||||||
SMTP_HOST=your.smtp.host.example.com
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=your-email@example.com
|
|
||||||
SMTP_PASS=your-smtp-password
|
|
||||||
|
|
||||||
# (Optional) Override the session_secret from bin/config.yaml.
|
|
||||||
# Leave commented out to use the value in config.yaml.
|
|
||||||
# APP_SECRET=change-me-in-production
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,6 +1,9 @@
|
||||||
# Environment
|
# Environment (legacy — app no longer reads .env, but guard against accidental commit)
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Stale build artifact from go build . (use go build ./src/cmd instead)
|
||||||
|
cmd
|
||||||
|
|
||||||
# Build artifacts (binary is distributed with the repo)
|
# Build artifacts (binary is distributed with the repo)
|
||||||
bin/*.log
|
bin/*.log
|
||||||
bin/db.sqlite
|
bin/db.sqlite
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,26 @@
|
||||||
# inBOXER Configuration
|
# inBOXER Configuration
|
||||||
|
# ======================
|
||||||
|
# Single configuration file — all settings including secrets live here.
|
||||||
|
# Replace placeholder values before deploying to production.
|
||||||
|
|
||||||
# Server configuration
|
# Server configuration
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
session_secret: "change-me-in-production" # Override with APP_SECRET from .env
|
session_secret: "change-me-in-production"
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
database:
|
database:
|
||||||
path: "bin/db.sqlite"
|
path: "bin/db.sqlite"
|
||||||
auto_migrate: true
|
auto_migrate: true
|
||||||
|
|
||||||
|
# SMTP credentials for sending OTP login emails
|
||||||
|
smtp:
|
||||||
|
host: "your.smtp.host.example.com"
|
||||||
|
port: 587
|
||||||
|
username: "your-email@example.com"
|
||||||
|
password: "your-smtp-password"
|
||||||
|
|
||||||
# IMAP configuration (user-specific, stored encrypted in database)
|
# IMAP configuration (user-specific, stored encrypted in database)
|
||||||
imap_defaults:
|
imap_defaults:
|
||||||
host: "imap.example.com"
|
host: "imap.example.com"
|
||||||
|
|
@ -22,6 +32,7 @@ imap_defaults:
|
||||||
# AI classification configuration
|
# AI classification configuration
|
||||||
ai:
|
ai:
|
||||||
model: "deepseek-chat"
|
model: "deepseek-chat"
|
||||||
|
api_key: "your_deepseek_api_key_here"
|
||||||
max_tokens: 1000
|
max_tokens: 1000
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
prompt_file: "bin/prompt.txt"
|
prompt_file: "bin/prompt.txt"
|
||||||
|
|
|
||||||
BIN
bin/inboxer
BIN
bin/inboxer
Binary file not shown.
5
go.mod
5
go.mod
|
|
@ -3,9 +3,9 @@ module inboxer
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/sessions v1.2.0
|
github.com/gorilla/sessions v1.2.0
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
golang.org/x/crypto v0.17.0
|
golang.org/x/crypto v0.17.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/sqlite v1.5.4
|
gorm.io/driver/sqlite v1.5.4
|
||||||
|
|
@ -13,10 +13,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/emersion/go-imap v1.2.1 // indirect
|
|
||||||
github.com/emersion/go-message v0.15.0 // indirect
|
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,10 +1,8 @@
|
||||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||||
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
|
|
||||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
|
@ -19,8 +17,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
|
|
|
||||||
57
install.sh
57
install.sh
|
|
@ -113,40 +113,6 @@ sed -i 's|file: "bin/inboxer.log"|file: "'"${LOGS_DIR}"'/inboxer.log"|' "${BIN_D
|
||||||
# prompt_file: "bin/prompt.txt" works as-is relative to the working directory,
|
# prompt_file: "bin/prompt.txt" works as-is relative to the working directory,
|
||||||
# since WorkingDirectory=/opt/inboxer resolves it to /opt/inboxer/bin/prompt.txt
|
# since WorkingDirectory=/opt/inboxer resolves it to /opt/inboxer/bin/prompt.txt
|
||||||
|
|
||||||
# ─── Create .env from template ──────────────────────────────────────────────
|
|
||||||
ENV_FILE="${INSTALL_DIR}/.env"
|
|
||||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
||||||
# Prefer .env.example from repo (single source of truth)
|
|
||||||
if [[ -f "${REPO_DIR}/.env.example" ]]; then
|
|
||||||
info "Copying .env.example from repository to ${ENV_FILE} ..."
|
|
||||||
cp "${REPO_DIR}/.env.example" "${ENV_FILE}"
|
|
||||||
else
|
|
||||||
info "Creating .env template at ${ENV_FILE} ..."
|
|
||||||
cat > "${ENV_FILE}" << 'ENVEOF'
|
|
||||||
# inBOXER Environment Configuration
|
|
||||||
# ====================================
|
|
||||||
# Set your credentials below. The service reads these variables on startup.
|
|
||||||
# You can also set them directly in the systemd EnvironmentFile or via
|
|
||||||
# the shell environment (godotenv.Load() checks both the .env file and os.Getenv).
|
|
||||||
|
|
||||||
# DeepSeek API key – used to classify incoming emails
|
|
||||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
|
||||||
|
|
||||||
# SMTP credentials – used to send one-time passwords (OTP) via email
|
|
||||||
SMTP_HOST=your.smtp.host
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=your-email@example.com
|
|
||||||
SMTP_PASS=your-smtp-password
|
|
||||||
|
|
||||||
# (Optional) Override the session_secret from config.yaml.
|
|
||||||
# APP_SECRET=change-me-in-production
|
|
||||||
ENVEOF
|
|
||||||
fi
|
|
||||||
info ".env file created."
|
|
||||||
else
|
|
||||||
info ".env already exists, keeping existing file."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─── Create systemd service unit ─────────────────────────────────────────────
|
# ─── Create systemd service unit ─────────────────────────────────────────────
|
||||||
info "Creating systemd service unit at ${SERVICE_FILE} ..."
|
info "Creating systemd service unit at ${SERVICE_FILE} ..."
|
||||||
cat > "${SERVICE_FILE}" << UNITEOF
|
cat > "${SERVICE_FILE}" << UNITEOF
|
||||||
|
|
@ -161,7 +127,6 @@ Type=simple
|
||||||
User=${SERVICE_USER}
|
User=${SERVICE_USER}
|
||||||
Group=${SERVICE_GROUP}
|
Group=${SERVICE_GROUP}
|
||||||
WorkingDirectory=${INSTALL_DIR}
|
WorkingDirectory=${INSTALL_DIR}
|
||||||
EnvironmentFile=${ENV_FILE}
|
|
||||||
ExecStart=${BIN_DIR}/inboxer
|
ExecStart=${BIN_DIR}/inboxer
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|
@ -189,8 +154,8 @@ chmod 755 "${BIN_DIR}"
|
||||||
chmod 750 "${DATA_DIR}" # database file is sensitive
|
chmod 750 "${DATA_DIR}" # database file is sensitive
|
||||||
chmod 750 "${LOGS_DIR}"
|
chmod 750 "${LOGS_DIR}"
|
||||||
|
|
||||||
# .env must be readable by the service user (and owner-only for secrets)
|
# config.yaml contains secrets so restrict access
|
||||||
chmod 640 "${ENV_FILE}"
|
chmod 640 "${BIN_DIR}/config.yaml"
|
||||||
|
|
||||||
# ─── Register & start service ───────────────────────────────────────────────
|
# ─── Register & start service ───────────────────────────────────────────────
|
||||||
info "Reloading systemd daemon..."
|
info "Reloading systemd daemon..."
|
||||||
|
|
@ -227,14 +192,20 @@ info " Configuration: ${BIN_DIR}/config.yaml"
|
||||||
info " Prompt file: ${BIN_DIR}/prompt.txt"
|
info " Prompt file: ${BIN_DIR}/prompt.txt"
|
||||||
info " Data (SQLite): ${DATA_DIR}/"
|
info " Data (SQLite): ${DATA_DIR}/"
|
||||||
info " Logs: ${LOGS_DIR}/"
|
info " Logs: ${LOGS_DIR}/"
|
||||||
info " Environment: ${ENV_FILE}"
|
|
||||||
echo ""
|
echo ""
|
||||||
info " Edit the environment file with your credentials:"
|
info " ⚙ Edit config.yaml with your credentials before first start:"
|
||||||
info " sudo nano ${ENV_FILE}"
|
info " sudo nano ${BIN_DIR}/config.yaml"
|
||||||
|
info ""
|
||||||
|
info " Required settings:"
|
||||||
|
info " - ai.api_key (DeepSeek API key)"
|
||||||
|
info " - smtp.host/port (SMTP server for OTP emails)"
|
||||||
|
info " - smtp.username (SMTP login)"
|
||||||
|
info " - smtp.password (SMTP password)"
|
||||||
|
info " - server.session_secret (change from the default)"
|
||||||
echo ""
|
echo ""
|
||||||
if grep -q "your_deepseek_api_key_here" "${ENV_FILE}" 2>/dev/null; then
|
if grep -q "your_deepseek_api_key_here\|your.smtp.host\|change-me-in-production" "${BIN_DIR}/config.yaml" 2>/dev/null; then
|
||||||
warn " ⚠ The .env file still contains placeholder values!"
|
warn " ⚠ config.yaml still contains placeholder values!"
|
||||||
warn " Edit ${ENV_FILE} before the service will function."
|
warn " Edit ${BIN_DIR}/config.yaml before the service will function."
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
info " Service management:"
|
info " Service management:"
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func main() {
|
||||||
configPath = "bin/config.yaml"
|
configPath = "bin/config.yaml"
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, env, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load configuration: %v", err)
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -38,19 +38,19 @@ func main() {
|
||||||
}
|
}
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
// Initialize SMTP sender
|
// Initialize SMTP sender (credentials from config.yaml)
|
||||||
smtpConfig := auth.SMTPConfig{
|
smtpConfig := auth.SMTPConfig{
|
||||||
Host: env.SMTPHost,
|
Host: cfg.SMTP.Host,
|
||||||
Port: env.SMTPPort,
|
Port: cfg.SMTP.Port,
|
||||||
Username: env.SMTPUser,
|
Username: cfg.SMTP.Username,
|
||||||
Password: env.SMTPPass,
|
Password: cfg.SMTP.Password,
|
||||||
From: env.SMTPUser,
|
From: cfg.SMTP.Username, // From address matches SMTP username
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate SMTP config (but don't fail if not set - user might configure later)
|
// Validate SMTP config (but don't fail if not set — user might configure later)
|
||||||
if err := smtpConfig.ValidateConfig(); err != nil {
|
if err := smtpConfig.ValidateConfig(); err != nil {
|
||||||
log.Printf("Warning: SMTP configuration incomplete: %v", err)
|
log.Printf("Warning: SMTP configuration incomplete: %v", err)
|
||||||
log.Println("OTP emails will not be sent until SMTP is configured in .env")
|
log.Println("OTP emails will not be sent until SMTP is configured in config.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
smtpSender := auth.NewSMTPSender(smtpConfig)
|
smtpSender := auth.NewSMTPSender(smtpConfig)
|
||||||
|
|
@ -70,7 +70,7 @@ func main() {
|
||||||
|
|
||||||
// Initialize AI classifier
|
// Initialize AI classifier
|
||||||
deepSeekAPI := ai.NewDeepSeekAPI(
|
deepSeekAPI := ai.NewDeepSeekAPI(
|
||||||
env.DeepSeekAPIKey,
|
cfg.AI.APIKey,
|
||||||
cfg.AI.Model,
|
cfg.AI.Model,
|
||||||
cfg.AI.MaxTokens,
|
cfg.AI.MaxTokens,
|
||||||
cfg.AI.Temperature,
|
cfg.AI.Temperature,
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ func TestInMemoryOTPStore(t *testing.T) {
|
||||||
func TestSMTPSenderValidation(t *testing.T) {
|
func TestSMTPSenderValidation(t *testing.T) {
|
||||||
config := SMTPConfig{
|
config := SMTPConfig{
|
||||||
Host: "smtp.example.com",
|
Host: "smtp.example.com",
|
||||||
Port: "587",
|
Port: 587,
|
||||||
Username: "user@example.com",
|
Username: "user@example.com",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
From: "user@example.com",
|
From: "user@example.com",
|
||||||
|
|
@ -123,11 +123,11 @@ func TestSMTPSenderValidation(t *testing.T) {
|
||||||
|
|
||||||
// Test invalid configs
|
// Test invalid configs
|
||||||
invalidConfigs := []SMTPConfig{
|
invalidConfigs := []SMTPConfig{
|
||||||
{Host: "", Port: "587", Username: "user", Password: "pass", From: "user"},
|
{Host: "", Port: 587, Username: "user", Password: "pass", From: "user"},
|
||||||
{Host: "smtp.example.com", Port: "", Username: "user", Password: "pass", From: "user"},
|
{Host: "smtp.example.com", Port: 0, Username: "user", Password: "pass", From: "user"},
|
||||||
{Host: "smtp.example.com", Port: "587", Username: "", Password: "pass", From: "user"},
|
{Host: "smtp.example.com", Port: 587, Username: "", Password: "pass", From: "user"},
|
||||||
{Host: "smtp.example.com", Port: "587", Username: "user", Password: "", From: "user"},
|
{Host: "smtp.example.com", Port: 587, Username: "user", Password: "", From: "user"},
|
||||||
{Host: "smtp.example.com", Port: "587", Username: "user", Password: "pass", From: ""},
|
{Host: "smtp.example.com", Port: 587, Username: "user", Password: "pass", From: ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range invalidConfigs {
|
for _, cfg := range invalidConfigs {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
// SMTPConfig holds SMTP server configuration
|
// SMTPConfig holds SMTP server configuration
|
||||||
type SMTPConfig struct {
|
type SMTPConfig struct {
|
||||||
Host string
|
Host string
|
||||||
Port string
|
Port int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
From string
|
From string
|
||||||
|
|
@ -51,7 +51,7 @@ func heloHostname(from, smtpHost string) string {
|
||||||
// setting the HELO/EHLO hostname explicitly. Some SMTP servers
|
// setting the HELO/EHLO hostname explicitly. Some SMTP servers
|
||||||
// reject messages when the HELO doesn't match a valid domain.
|
// reject messages when the HELO doesn't match a valid domain.
|
||||||
func (s *SMTPSender) sendMail(to []string, msg []byte) error {
|
func (s *SMTPSender) sendMail(to []string, msg []byte) error {
|
||||||
addr := fmt.Sprintf("%s:%s", s.config.Host, s.config.Port)
|
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
|
||||||
helo := heloHostname(s.config.From, s.config.Host)
|
helo := heloHostname(s.config.From, s.config.Host)
|
||||||
|
|
||||||
// Connect to the SMTP server
|
// Connect to the SMTP server
|
||||||
|
|
@ -159,7 +159,7 @@ func (s *SMTPConfig) ValidateConfig() error {
|
||||||
if s.Host == "" {
|
if s.Host == "" {
|
||||||
errors = append(errors, "SMTP host is required")
|
errors = append(errors, "SMTP host is required")
|
||||||
}
|
}
|
||||||
if s.Port == "" {
|
if s.Port == 0 {
|
||||||
errors = append(errors, "SMTP port is required")
|
errors = append(errors, "SMTP port is required")
|
||||||
}
|
}
|
||||||
if s.Username == "" {
|
if s.Username == "" {
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,15 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config represents the application configuration
|
// Config represents the application configuration.
|
||||||
|
// All settings live in a single config.yaml — no separate .env file.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
SMTP SMTPSettings `yaml:"smtp"`
|
||||||
IMAP IMAPConfig `yaml:"imap_defaults"`
|
IMAP IMAPConfig `yaml:"imap_defaults"`
|
||||||
AI AIConfig `yaml:"ai"`
|
AI AIConfig `yaml:"ai"`
|
||||||
Worker WorkerConfig `yaml:"worker"`
|
Worker WorkerConfig `yaml:"worker"`
|
||||||
|
|
@ -22,40 +23,51 @@ type Config struct {
|
||||||
|
|
||||||
// ServerConfig holds web server configuration
|
// ServerConfig holds web server configuration
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
SessionSecret string `yaml:"session_secret"`
|
SessionSecret string `yaml:"session_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseConfig holds database configuration
|
// DatabaseConfig holds database configuration
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
AutoMigrate bool `yaml:"auto_migrate"`
|
AutoMigrate bool `yaml:"auto_migrate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SMTPSettings holds SMTP credentials for sending OTP emails.
|
||||||
|
// Previously stored in a separate .env file; now in config.yaml.
|
||||||
|
type SMTPSettings struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
// IMAPConfig holds default IMAP configuration
|
// IMAPConfig holds default IMAP configuration
|
||||||
type IMAPConfig struct {
|
type IMAPConfig struct {
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
TLS bool `yaml:"tls"`
|
TLS bool `yaml:"tls"`
|
||||||
BatchSize int `yaml:"batch_size"`
|
BatchSize int `yaml:"batch_size"`
|
||||||
PollIntervalMinutes int `yaml:"poll_interval_minutes"`
|
PollIntervalMinutes int `yaml:"poll_interval_minutes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AIConfig holds AI classification configuration
|
// AIConfig holds AI classification configuration.
|
||||||
|
// api_key was previously in .env; now stored here.
|
||||||
type AIConfig struct {
|
type AIConfig struct {
|
||||||
Model string `yaml:"model"`
|
Model string `yaml:"model"`
|
||||||
MaxTokens int `yaml:"max_tokens"`
|
MaxTokens int `yaml:"max_tokens"`
|
||||||
Temperature float64 `yaml:"temperature"`
|
Temperature float64 `yaml:"temperature"`
|
||||||
PromptFile string `yaml:"prompt_file"`
|
PromptFile string `yaml:"prompt_file"`
|
||||||
|
APIKey string `yaml:"api_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkerConfig holds background worker configuration
|
// WorkerConfig holds background worker configuration
|
||||||
type WorkerConfig struct {
|
type WorkerConfig struct {
|
||||||
SteadyStateBatchSize int `yaml:"steady_state_batch_size"`
|
SteadyStateBatchSize int `yaml:"steady_state_batch_size"`
|
||||||
SteadyStateIntervalMinutes int `yaml:"steady_state_interval_minutes"`
|
SteadyStateIntervalMinutes int `yaml:"steady_state_interval_minutes"`
|
||||||
CatchUpBatchSize int `yaml:"catch_up_batch_size"`
|
CatchUpBatchSize int `yaml:"catch_up_batch_size"`
|
||||||
CatchUpCooldownSeconds int `yaml:"catch_up_cooldown_seconds"`
|
CatchUpCooldownSeconds int `yaml:"catch_up_cooldown_seconds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FolderConfig holds email folder names
|
// FolderConfig holds email folder names
|
||||||
|
|
@ -71,68 +83,37 @@ type FolderConfig struct {
|
||||||
|
|
||||||
// LoggingConfig holds logging configuration
|
// LoggingConfig holds logging configuration
|
||||||
type LoggingConfig struct {
|
type LoggingConfig struct {
|
||||||
Level string `yaml:"level"`
|
Level string `yaml:"level"`
|
||||||
File string `yaml:"file"`
|
File string `yaml:"file"`
|
||||||
MaxSizeMB int `yaml:"max_size_mb"`
|
MaxSizeMB int `yaml:"max_size_mb"`
|
||||||
MaxBackups int `yaml:"max_backups"`
|
MaxBackups int `yaml:"max_backups"`
|
||||||
MaxAgeDays int `yaml:"max_age_days"`
|
MaxAgeDays int `yaml:"max_age_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment variables
|
// LoadConfig loads configuration from a YAML file.
|
||||||
type Environment struct {
|
// No .env file is consulted — everything is in config.yaml.
|
||||||
DeepSeekAPIKey string
|
func LoadConfig(configPath string) (*Config, error) {
|
||||||
SMTPHost string
|
|
||||||
SMTPPort string
|
|
||||||
SMTPUser string
|
|
||||||
SMTPPass string
|
|
||||||
AppSecret string
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadConfig loads configuration from YAML file and environment variables
|
|
||||||
func LoadConfig(configPath string) (*Config, *Environment, error) {
|
|
||||||
// Load .env file
|
|
||||||
if err := godotenv.Load(); err != nil {
|
|
||||||
// It's okay if .env doesn't exist in production
|
|
||||||
fmt.Printf("Note: .env file not found: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read YAML config
|
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to read config file: %w", err)
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to parse config file: %w", err)
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load environment variables
|
return &config, nil
|
||||||
env := &Environment{
|
|
||||||
DeepSeekAPIKey: os.Getenv("DEEPSEEK_API_KEY"),
|
|
||||||
SMTPHost: os.Getenv("SMTP_HOST"),
|
|
||||||
SMTPPort: os.Getenv("SMTP_PORT"),
|
|
||||||
SMTPUser: os.Getenv("SMTP_USER"),
|
|
||||||
SMTPPass: os.Getenv("SMTP_PASS"),
|
|
||||||
AppSecret: os.Getenv("APP_SECRET"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use APP_SECRET from environment if available
|
|
||||||
if env.AppSecret != "" {
|
|
||||||
config.Server.SessionSecret = env.AppSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
return &config, env, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultConfigPath returns the default path to config.yaml
|
// GetDefaultConfigPath returns the default path to config.yaml.
|
||||||
|
// It looks for config.yaml in the same directory as the executable.
|
||||||
func GetDefaultConfigPath() (string, error) {
|
func GetDefaultConfigPath() (string, error) {
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to relative path
|
|
||||||
return "bin/config.yaml", nil
|
return "bin/config.yaml", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
execDir := filepath.Dir(execPath)
|
execDir := filepath.Dir(execPath)
|
||||||
return filepath.Join(execDir, "config.yaml"), nil
|
return filepath.Join(execDir, "config.yaml"), nil
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue