Handle config, parse flags
This commit is contained in:
parent
338a07a577
commit
449a8a2c67
6 changed files with 375 additions and 8 deletions
|
|
@ -31,12 +31,12 @@ func main() {
|
|||
fmt.Println("Mail Tester - Email Deliverability Testing Platform")
|
||||
fmt.Println("Version: 0.1.0-dev")
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
cfg, err := config.ConsolidateConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
command := flag.Arg(0)
|
||||
|
||||
switch command {
|
||||
case "server":
|
||||
|
|
@ -55,8 +55,10 @@ func main() {
|
|||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("\nUsage:")
|
||||
fmt.Println(" mailtester server - Start the API server")
|
||||
fmt.Println(" mailtester analyze - Start the email analyzer (MDA mode)")
|
||||
fmt.Println(" mailtester version - Print version information")
|
||||
fmt.Println("\nCommand availables:")
|
||||
fmt.Println(" happyDeliver server - Start the API server")
|
||||
fmt.Println(" happyDeliver analyze [-recipient EMAIL] - Analyze email from stdin (MDA mode)")
|
||||
fmt.Println(" happyDeliver version - Print version information")
|
||||
fmt.Println("")
|
||||
flag.Usage()
|
||||
}
|
||||
|
|
|
|||
48
internal/config/cli.go
Normal file
48
internal/config/cli.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
// declareFlags registers flags for the structure Options.
|
||||
func declareFlags(o *Config) {
|
||||
flag.StringVar(&o.DevProxy, "dev", o.DevProxy, "Proxify traffic to this host for static assets")
|
||||
flag.StringVar(&o.Bind, "bind", o.Bind, "Bind port/socket")
|
||||
flag.StringVar(&o.Database.Type, "database-type", o.Database.Type, "Select the database type between sqlite, postgres")
|
||||
flag.StringVar(&o.Database.DSN, "database-dsn", o.Database.DSN, "Database DSN or path")
|
||||
flag.StringVar(&o.Email.Domain, "domain", o.Email.Domain, "Domain used to receive emails")
|
||||
flag.StringVar(&o.Email.TestAddressPrefix, "address-prefix", o.Email.TestAddressPrefix, "Expected email adress prefix (deny address that doesn't start with this prefix)")
|
||||
flag.DurationVar(&o.Analysis.DNSTimeout, "dns-timeout", o.Analysis.DNSTimeout, "Timeout when performing DNS query")
|
||||
flag.DurationVar(&o.Analysis.HTTPTimeout, "http-timeout", o.Analysis.HTTPTimeout, "Timeout when performing HTTP query")
|
||||
flag.Var(&StringArray{&o.Analysis.RBLs}, "rbl", "Append a RBL (use this option multiple time to append multiple RBLs)")
|
||||
|
||||
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
|
||||
}
|
||||
|
||||
// parseCLI parse the flags and treats extra args as configuration filename.
|
||||
func parseCLI(o *Config) error {
|
||||
flag.Parse()
|
||||
|
||||
return nil
|
||||
}
|
||||
176
internal/config/config.go
Normal file
176
internal/config/config.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
openapi_types "github.com/oapi-codegen/runtime/types"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
DevProxy string
|
||||
Bind string
|
||||
Database DatabaseConfig
|
||||
Email EmailConfig
|
||||
Analysis AnalysisConfig
|
||||
}
|
||||
|
||||
// DatabaseConfig contains database connection settings
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
DSN string
|
||||
}
|
||||
|
||||
// EmailConfig contains email domain and routing settings
|
||||
type EmailConfig struct {
|
||||
Domain string
|
||||
TestAddressPrefix string
|
||||
}
|
||||
|
||||
// AnalysisConfig contains timeout and behavior settings for email analysis
|
||||
type AnalysisConfig struct {
|
||||
DNSTimeout time.Duration
|
||||
HTTPTimeout time.Duration
|
||||
RBLs []string
|
||||
}
|
||||
|
||||
// DefaultConfig returns a configuration with sensible defaults
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
DevProxy: "",
|
||||
Bind: ":8080",
|
||||
Database: DatabaseConfig{
|
||||
Type: "sqlite",
|
||||
DSN: "happydeliver.db",
|
||||
},
|
||||
Email: EmailConfig{
|
||||
Domain: "happydeliver.local",
|
||||
TestAddressPrefix: "test-",
|
||||
},
|
||||
Analysis: AnalysisConfig{
|
||||
DNSTimeout: 5 * time.Second,
|
||||
HTTPTimeout: 10 * time.Second,
|
||||
RBLs: []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ConsolidateConfig fills an Options struct by reading configuration from
|
||||
// config files, environment, then command line.
|
||||
//
|
||||
// Should be called only one time.
|
||||
func ConsolidateConfig() (opts *Config, err error) {
|
||||
// Define defaults options
|
||||
opts = DefaultConfig()
|
||||
|
||||
declareFlags(opts)
|
||||
|
||||
// Establish a list of possible configuration file locations
|
||||
configLocations := []string{
|
||||
"happydeliver.conf",
|
||||
}
|
||||
|
||||
if home, err := os.UserConfigDir(); err == nil {
|
||||
configLocations = append(
|
||||
configLocations,
|
||||
path.Join(home, "happydeliver", "happydeliver.conf"),
|
||||
path.Join(home, "happydomain", "happydeliver.conf"),
|
||||
)
|
||||
}
|
||||
|
||||
configLocations = append(configLocations, path.Join("etc", "happydeliver.conf"))
|
||||
|
||||
// If config file exists, read configuration from it
|
||||
for _, filename := range configLocations {
|
||||
if _, e := os.Stat(filename); !os.IsNotExist(e) && !os.IsPermission(e) {
|
||||
log.Printf("Loading configuration from %s\n", filename)
|
||||
err = parseFile(opts, filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Then, overwrite that by what is present in the environment
|
||||
err = parseEnvironmentVariables(opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Finaly, command line takes precedence
|
||||
err = parseCLI(opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *Config) Validate() error {
|
||||
if c.Email.Domain == "" {
|
||||
return fmt.Errorf("email domain cannot be empty")
|
||||
}
|
||||
|
||||
if _, err := openapi_types.Email(fmt.Sprintf("%s1234-5678-9090@%s", c.Email.TestAddressPrefix, c.Email.Domain)).MarshalJSON(); err != nil {
|
||||
return fmt.Errorf("invalid email domain: %w", err)
|
||||
}
|
||||
|
||||
if c.Database.Type != "sqlite" && c.Database.Type != "postgres" {
|
||||
return fmt.Errorf("unsupported database type: %s", c.Database.Type)
|
||||
}
|
||||
|
||||
if c.Database.DSN == "" {
|
||||
return fmt.Errorf("database DSN cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLine treats a config line and place the read value in the variable
|
||||
// declared to the corresponding flag.
|
||||
func parseLine(o *Config, line string) (err error) {
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
orig_key := strings.TrimSpace(fields[0])
|
||||
value := strings.TrimSpace(fields[1])
|
||||
|
||||
if len(value) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
key := strings.TrimPrefix(strings.TrimPrefix(orig_key, "HAPPYDELIVER_"), "HAPPYDOMAIN_")
|
||||
key = strings.Replace(key, "_", "-", -1)
|
||||
key = strings.ToLower(key)
|
||||
|
||||
err = flag.Set(key, value)
|
||||
|
||||
return
|
||||
}
|
||||
45
internal/config/custom.go
Normal file
45
internal/config/custom.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StringArray struct {
|
||||
Array *[]string
|
||||
}
|
||||
|
||||
func (i *StringArray) String() string {
|
||||
if i.Array == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", *i.Array)
|
||||
}
|
||||
|
||||
func (i *StringArray) Set(value string) error {
|
||||
*i.Array = append(*i.Array, strings.Split(value, ",")...)
|
||||
|
||||
return nil
|
||||
}
|
||||
42
internal/config/env.go
Normal file
42
internal/config/env.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseEnvironmentVariables analyzes all the environment variables to find
|
||||
// each one starting by HAPPYDELIVER_
|
||||
func parseEnvironmentVariables(o *Config) (err error) {
|
||||
for _, line := range os.Environ() {
|
||||
if strings.HasPrefix(line, "HAPPYDELIVER_") || strings.HasPrefix(line, "HAPPYDOMAIN_") {
|
||||
err := parseLine(o, line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in environment (%q): %w", line, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
54
internal/config/file.go
Normal file
54
internal/config/file.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseFile opens the file at the given filename path, then treat each line
|
||||
// not starting with '#' as a configuration statement.
|
||||
func parseFile(o *Config, filename string) error {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
scanner := bufio.NewScanner(fp)
|
||||
n := 0
|
||||
for scanner.Scan() {
|
||||
n += 1
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
err := parseLine(o, line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue