Refactor in multiple files. First working version.

This commit is contained in:
sfigato 2020-02-10 21:48:41 +01:00
parent 4d4454a14c
commit 02ab2ec867
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
8 changed files with 425 additions and 162 deletions

126
config.go Normal file
View File

@ -0,0 +1,126 @@
package main
import (
"errors"
"fmt"
"os"
toml "github.com/pelletier/go-toml"
)
type Validation struct {
Param string
CmdFlag string
ConfFlag string
}
type ServerConfig struct {
Address string `toml:address,omitempty`
Port int64 `toml:port,omitempty`
Encryption bool `toml:encryption,omitempty`
User string `toml:user,omitempty`
Password string `toml:password,omitempty`
}
func (s ServerConfig) String() string {
return fmt.Sprintf(
"\tAddress: %s\n\tPort: %d\n\tEncryption: %t\n\tUser: %s\n\tPassword: %s\n",
s.Address,
s.Port,
s.Encryption,
s.User,
s.Password,
)
}
type Config struct {
Server *ServerConfig `toml:server,omitempty`
From string `toml:from,omitempty`
To []string `toml:to,omitempty`
Cc []string `toml:cc,omitempty`
Bcc []string `toml:bcc,omitempty`
Subject string `toml:subject,omitempty`
Text string `toml:text,omitempty`
}
func (c Config) String() string {
return fmt.Sprintf(
"From: %s\nTo: %s\nCc: %s\nBcc: %s\nSubject: %s\nText:\n%s\nServer:\n%s",
c.From,
c.To,
c.Cc,
c.Bcc,
c.Subject,
c.Text,
c.Server,
)
}
func NewConfig() *Config {
server := &ServerConfig{}
config := &Config{}
config.Server = server
return config
}
func (s *ServerConfig) Merge(key string, value interface{}) {
Merge(key, value, s)
}
func (c *Config) Merge(key string, value interface{}) {
Merge(key, value, c)
}
func readConfig(configPath, section string) *Config {
config := NewConfig()
tree, err := toml.LoadFile(configPath)
if err != nil {
Error.F("Error in parsing the configuration\n%s", err)
os.Exit(2)
}
keys := tree.Keys()
if !IsPresent(keys, section) {
return config
}
sub := tree.Get(section).(*toml.Tree)
err = sub.Unmarshal(config)
return config
}
func checkValidity(validations *[]Validation, obj interface{}, name, cmd, param string) {
if IsEmpty(obj) {
*validations = append(*validations, Validation{name, cmd, param})
}
}
func (c *Config) Validate() error {
var validations = []Validation{}
var msg string
checkValidity(&validations, c.Server.Address, "server address", "server-address", "server.address")
checkValidity(&validations, c.Server.Port, "server port", "server-port", "server.port")
if !c.Server.Encryption {
Warning.Ln("Warning: not using encryption!")
}
checkValidity(&validations, c.Server.User, "username", "user", "server.user")
checkValidity(&validations, c.Server.Password, "password", "password", "server.password")
checkValidity(&validations, c.From, "from", "from", "from")
checkValidity(&validations, c.To, "to", "to", "to")
checkValidity(&validations, c.Subject, "subject", "sub", "subject")
checkValidity(&validations, c.Text, "body", "", "text")
Debug.F("Lengths:\n\tTo: %d\n\tCc: %d\n\tBcc: %d", len(c.To), len(c.Cc), len(c.Bcc))
if len(validations) > 0 {
for _, v := range validations {
if v.CmdFlag == "" {
msg += fmt.Sprintf("%s: pass a value either via stdin or in configuration file section (%s)\n", v.Param, v.ConfFlag)
} else {
msg += fmt.Sprintf("%s: pass a value either via command line (-%s) or in configuration file section (%s)\n", v.Param, v.CmdFlag, v.ConfFlag)
}
}
return errors.New(msg)
}
return nil
}

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module git.lattuga.net/blallo/sendmail
go 1.13
require (
github.com/fatih/color v1.9.0
github.com/pelletier/go-toml v1.6.0
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1
)

24
go.sum Normal file
View File

@ -0,0 +1,24 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

96
log.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"io"
"io/ioutil"
"log"
"os"
"github.com/fatih/color"
)
type DebugLog struct {
logger *log.Logger
}
type InfoLog struct {
logger *log.Logger
}
type WarningLog struct {
logger *log.Logger
}
type ErrorLog struct {
logger *log.Logger
}
var (
Debug = &DebugLog{}
Info = &InfoLog{}
Warning = &WarningLog{}
Error = &ErrorLog{}
)
func (l *DebugLog) F(fmt string, text ...interface{}) {
l.logger.Print(color.MagentaString(fmt, text...))
}
func (l *DebugLog) Ln(text ...interface{}) {
l.logger.Print(color.MagentaString("%s", text...))
}
func (l *InfoLog) F(fmt string, text ...interface{}) {
l.logger.Print(color.WhiteString(fmt, text...))
}
func (l *InfoLog) Ln(text ...interface{}) {
l.logger.Print(color.WhiteString("%s", text...))
}
func (l *WarningLog) F(fmt string, text ...interface{}) {
l.logger.Print(color.YellowString(fmt, text...))
}
func (l *WarningLog) Ln(text ...interface{}) {
l.logger.Print(color.YellowString("%s", text...))
}
func (l *ErrorLog) F(fmt string, text ...interface{}) {
l.logger.Print(color.RedString(fmt, text...))
}
func (l *ErrorLog) Ln(text ...interface{}) {
l.logger.Print(color.RedString("%s", text...))
}
func (l *DebugLog) init(w io.Writer, prefix string, flag int) {
l.logger = &log.Logger{}
l.logger = log.New(w, prefix, flag)
}
func (l *InfoLog) init(w io.Writer, prefix string, flag int) {
l.logger = &log.Logger{}
l.logger = log.New(w, prefix, flag)
}
func (l *WarningLog) init(w io.Writer, prefix string, flag int) {
l.logger = &log.Logger{}
l.logger = log.New(w, prefix, flag)
}
func (l *ErrorLog) init(w io.Writer, prefix string, flag int) {
l.logger = &log.Logger{}
l.logger = log.New(w, prefix, flag)
}
func LogInit(debug bool) {
var debugOut io.Writer
if debug {
debugOut = os.Stdout
} else {
debugOut = ioutil.Discard
}
Debug.init(debugOut, "", 0)
Info.init(os.Stdout, "", 0)
Warning.init(os.Stdout, "", 0)
Error.init(os.Stderr, "", 0)
}

45
mail.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"os"
mail "gopkg.in/mail.v2"
)
func formatMessage(config *Config) *mail.Message {
m := mail.NewMessage()
m.SetHeader("From", config.From)
m.SetHeader("To", config.To...)
if !IsEmpty(config.Cc) {
m.SetHeader("Cc", config.Cc...)
}
if !IsEmpty(config.Bcc) {
m.SetHeader("Bcc", config.Bcc...)
}
m.SetHeader("Subject", config.Subject)
m.SetBody("text/plain", config.Text)
Debug.F("Message to deliver:\n%s", m)
return m
}
func deliverMessage(s *ServerConfig, m *mail.Message) error {
dialer := mail.NewDialer(
s.Address,
int(s.Port),
s.User,
s.Password,
)
if s.Encryption {
dialer.StartTLSPolicy = mail.MandatoryStartTLS
}
return dialer.DialAndSend(m)
}
func SendMail(config *Config) {
m := formatMessage(config)
if err := deliverMessage(config.Server, m); err != nil {
Error.F("Delivery failure:\n%s", err)
os.Exit(3)
}
}

227
main.go
View File

@ -1,155 +1,47 @@
package main package main
import ( import (
"bufio"
"flag" "flag"
"fmt" "fmt"
"log" "os"
"reflect"
"strings" "strings"
toml "github.com/pelletier/go-toml"
// mail "gopkg.in/gomail.v2"
) )
type ServerConfig struct { func readFromConsole() string {
Address string `toml:address,omitempty` var text, line string
Port int64 `toml:port,omitempty` var err error
Encryption string `toml:encryption,omitempty` counter := 0
User string `toml:user,omitempty` reader := bufio.NewReader(os.Stdin)
Password string `toml:password,omitempty` for counter < 3 {
} line, err = reader.ReadString('\n')
if line == "\n" {
func (s ServerConfig) String() string { counter += 1
return fmt.Sprintf(
"\tAddress: %s\n\tPort: %d\n\tEncryption: %s\n\tUser: %s\n\tPassword: %s\n",
s.Address,
s.Port,
s.Encryption,
s.User,
s.Password,
)
}
type Config struct {
Server *ServerConfig `toml:server,omitempty`
From string `toml:from,omitempty`
To []string `toml:to,omitempty`
Cc []string `toml:cc,omitempty`
Bcc []string `toml:bcc,omitempty`
Subject string `toml:subject,omitempty`
Text string `toml:text,omitempty`
}
func (c Config) String() string {
return fmt.Sprintf(
"From: %s\nTo: %s\nCc: %s\nBcc: %s\nSubject: %s\nText:\n%s\nServer:\n%s",
c.From,
c.To,
c.Cc,
c.Bcc,
c.Subject,
c.Text,
c.Server,
)
}
func NewConfig() *Config {
server := &ServerConfig{}
config := &Config{}
config.Server = server
return config
}
func debug(active bool, fmt string, msg ...interface{}) {
if active {
log.Printf(fmt, msg...)
}
}
func isPresent(slice []string, val string) bool {
for _, item := range slice {
if item == val {
return true
} }
text += fmt.Sprintf("%s\n", line)
} }
return false
}
func isEmpty(value interface{}) bool {
t := reflect.TypeOf(value)
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Slice:
return v.Len() == 1
default:
return value == reflect.Zero(t).Interface()
}
}
func readConfig(configPath, section string) *Config {
config := NewConfig()
tree, err := toml.LoadFile(configPath)
if err != nil { if err != nil {
log.Panicln(err) Error.F("Error in reading text from console\n%s", err)
os.Exit(1)
} }
keys := tree.Keys() return text
if !isPresent(keys, section) {
return config
}
sub := tree.Get(section).(*toml.Tree)
err = sub.Unmarshal(config)
return config
} }
func (c *Config) Validate() error { func toList(input string) []string {
return nil var result = []string{}
} list := strings.Split(input, ",")
for _, element := range list {
func merge(key string, value, obj interface{}) { if element != "" {
if isEmpty(value) { result = append(result, element)
return
}
var elem reflect.Value
if reflect.TypeOf(obj) != reflect.TypeOf(reflect.Value{}) {
log.Println("Not Value")
elem = reflect.ValueOf(obj).Elem()
} else {
log.Println("Value")
elem = obj.(reflect.Value)
}
field := elem.FieldByName(key)
if field.CanSet() {
switch field.Kind() {
case reflect.Int64:
field.SetInt(value.(int64))
case reflect.Bool:
field.SetBool(value.(bool))
case reflect.String:
field.SetString(value.(string))
case reflect.Slice:
field.Set(reflect.ValueOf(value))
default:
merge(key, reflect.ValueOf(value), field)
//merge(key, value, field.Pointer())
} }
} }
} return result
func (s *ServerConfig) Merge(key string, value interface{}) {
merge(key, value, s)
}
func (c *Config) Merge(key string, value interface{}) {
merge(key, value, c)
} }
func main() { func main() {
//var err error var err error
var configPath, section, serverAddress, encryption, user, password, to, cc, bcc, from, subject string var configPath, section, serverAddress, user, password, to, cc, bcc, from, subject, text string
var dbg bool var encryption, dbg bool
var serverPort_ int var serverPort_ int
var serverPort int64 var serverPort int64
flag.StringVar(&configPath, "conf", "/etc/sendmail.toml", "Path to a config file (defaults to /etc/sendmail.toml)") flag.StringVar(&configPath, "conf", "/etc/sendmail.toml", "Path to a config file (defaults to /etc/sendmail.toml)")
@ -157,7 +49,7 @@ func main() {
flag.BoolVar(&dbg, "dbg", false, "Enable debugging output") flag.BoolVar(&dbg, "dbg", false, "Enable debugging output")
flag.StringVar(&serverAddress, "server-address", "", "The SMTP server address") flag.StringVar(&serverAddress, "server-address", "", "The SMTP server address")
flag.IntVar(&serverPort_, "server-port", 0, "The SMTP server") flag.IntVar(&serverPort_, "server-port", 0, "The SMTP server")
flag.StringVar(&encryption, "encryption", "", "The encryption type (no, starttls, tls)") flag.BoolVar(&encryption, "force-ssl", false, "Force the use of ssl (defalut: false)")
flag.StringVar(&user, "user", "", "The user to authenticate with to the server") flag.StringVar(&user, "user", "", "The user to authenticate with to the server")
flag.StringVar(&password, "password", "", "The password to authenticate with to the server") flag.StringVar(&password, "password", "", "The password to authenticate with to the server")
flag.StringVar(&to, "to", "", "Comma-separated list of recipient(s)") flag.StringVar(&to, "to", "", "Comma-separated list of recipient(s)")
@ -167,20 +59,31 @@ func main() {
flag.StringVar(&subject, "sub", "", "Subject of the mail") flag.StringVar(&subject, "sub", "", "Subject of the mail")
flag.Parse() flag.Parse()
LogInit(dbg)
if flag.NArg() == 0 {
text = readFromConsole()
} else {
for _, arg := range flag.Args() {
text += fmt.Sprintf("%s\n", arg)
}
}
serverPort = int64(serverPort_) serverPort = int64(serverPort_)
debug( Debug.F(
dbg, `
`parameters: ---
conf: %s parameters:
dbg: %t conf: %s
address: %s dbg: %t
port: %d address: %s
encryption: %s port: %d
user: %s encryption: %t
password: %s user: %s
to: %s password: %s
from: %s to: %s
subject: %s`, from: %s
subject: %s`,
configPath, configPath,
dbg, dbg,
serverAddress, serverAddress,
@ -193,21 +96,27 @@ func main() {
subject, subject,
) )
config := readConfig(configPath, section) config := readConfig(configPath, section)
debug(dbg, "\n%s", *config) Debug.F("---\nConfig from %s\n%s", configPath, *config)
// config.Server.Merge("Address", serverAddress) config.Server.Merge("Address", serverAddress)
// config.Server.Merge("Port", serverPort) config.Server.Merge("Port", serverPort)
// config.Server.Merge("Encryption", encryption) config.Server.Merge("Encryption", encryption)
// config.Server.Merge("User", user) config.Server.Merge("User", user)
// config.Server.Merge("Password", password) config.Server.Merge("Password", password)
config.Merge("Server", &ServerConfig{serverAddress, serverPort, encryption, user, password})
config.Merge("From", from) config.Merge("From", from)
config.Merge("To", strings.Split(to, ",")) config.Merge("To", toList(to))
config.Merge("Cc", strings.Split(cc, ",")) config.Merge("Cc", toList(cc))
config.Merge("Bcc", strings.Split(bcc, ",")) config.Merge("Bcc", toList(bcc))
config.Merge("Subject", subject) config.Merge("Subject", subject)
// config.Merge("Text", text) config.Merge("Text", text)
debug(dbg, "\n%s", config) Debug.F("---\nPre-validation config\n%s", config)
err = config.Validate()
if err != nil {
Error.F("Config validation failed:\n%s\n", err)
os.Exit(1)
}
Info.F("Sending mail | to: %s", config.To)
SendMail(config)
} }

View File

@ -16,7 +16,7 @@
[default.server] [default.server]
address = "1.3.1.2" address = "1.3.1.2"
port = 587 port = 587
encryption = "tls" encryption = true
user = "faggiano@uccelli.net" user = "faggiano@uccelli.net"
password = "yougetit" password = "yougetit"
@ -38,7 +38,7 @@
[personal.server] [personal.server]
address = "10.13.12.10" address = "10.13.12.10"
port = 25 port = 25
encryption = "starttls" encryption = true
user = "master@domain.local" user = "master@domain.local"
password = "yougetitagain" password = "yougetitagain"
@ -46,5 +46,5 @@
[secret.server] [secret.server]
address = "my.server.org" address = "my.server.org"
port = 12345 port = 12345
encryption = "tls" encryption = false
user = "me" user = "me"

53
utils.go Normal file
View File

@ -0,0 +1,53 @@
package main
import "reflect"
func IsPresent(slice []string, val string) bool {
for _, item := range slice {
if item == val {
return true
}
}
return false
}
func IsEmpty(value interface{}) bool {
t := reflect.TypeOf(value)
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Slice:
return v.Len() == 0
default:
return value == reflect.Zero(t).Interface()
}
}
func Merge(key string, value, obj interface{}) {
if IsEmpty(value) {
return
}
field := reflect.ValueOf(obj).Elem().FieldByName(key)
if field.CanSet() {
switch field.Kind() {
case reflect.Int:
field.Set(reflect.ValueOf(value))
case reflect.Int16:
field.Set(reflect.ValueOf(value))
case reflect.Int32:
field.Set(reflect.ValueOf(value))
case reflect.Int64:
field.SetInt(value.(int64))
case reflect.Bool:
field.SetBool(value.(bool))
case reflect.String:
field.SetString(value.(string))
case reflect.Slice:
field.Set(reflect.ValueOf(value))
case reflect.Ptr:
field.Set(reflect.ValueOf(value))
default:
Merge(key, value, field)
}
}
}