diff --git a/config.go b/config.go new file mode 100644 index 0000000..89bf729 --- /dev/null +++ b/config.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b3dff2 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4c6e769 --- /dev/null +++ b/go.sum @@ -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= diff --git a/log.go b/log.go new file mode 100644 index 0000000..74513de --- /dev/null +++ b/log.go @@ -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) +} diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..785d986 --- /dev/null +++ b/mail.go @@ -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) + } +} diff --git a/main.go b/main.go index 9b142d2..b4c65f6 100644 --- a/main.go +++ b/main.go @@ -1,155 +1,47 @@ package main import ( + "bufio" "flag" "fmt" - "log" - "reflect" + "os" "strings" - - toml "github.com/pelletier/go-toml" - // mail "gopkg.in/gomail.v2" ) -type ServerConfig struct { - Address string `toml:address,omitempty` - Port int64 `toml:port,omitempty` - Encryption string `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: %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 +func readFromConsole() string { + var text, line string + var err error + counter := 0 + reader := bufio.NewReader(os.Stdin) + for counter < 3 { + line, err = reader.ReadString('\n') + if line == "\n" { + counter += 1 } + 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 { - log.Panicln(err) + Error.F("Error in reading text from console\n%s", err) + os.Exit(1) } - keys := tree.Keys() - - if !isPresent(keys, section) { - return config - } - sub := tree.Get(section).(*toml.Tree) - err = sub.Unmarshal(config) - return config + return text } -func (c *Config) Validate() error { - return nil -} - -func merge(key string, value, obj interface{}) { - if isEmpty(value) { - 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()) +func toList(input string) []string { + var result = []string{} + list := strings.Split(input, ",") + for _, element := range list { + if element != "" { + result = append(result, element) } } -} - -func (s *ServerConfig) Merge(key string, value interface{}) { - merge(key, value, s) -} - -func (c *Config) Merge(key string, value interface{}) { - merge(key, value, c) + return result } func main() { - //var err error - var configPath, section, serverAddress, encryption, user, password, to, cc, bcc, from, subject string - var dbg bool + var err error + var configPath, section, serverAddress, user, password, to, cc, bcc, from, subject, text string + var encryption, dbg bool var serverPort_ int var serverPort int64 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.StringVar(&serverAddress, "server-address", "", "The SMTP server address") 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(&password, "password", "", "The password to authenticate with to the server") 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.Parse() + LogInit(dbg) + + if flag.NArg() == 0 { + text = readFromConsole() + } else { + for _, arg := range flag.Args() { + text += fmt.Sprintf("%s\n", arg) + } + } + serverPort = int64(serverPort_) - debug( - dbg, - `parameters: - conf: %s - dbg: %t - address: %s - port: %d - encryption: %s - user: %s - password: %s - to: %s - from: %s - subject: %s`, + Debug.F( + ` +--- +parameters: + conf: %s + dbg: %t + address: %s + port: %d + encryption: %t + user: %s + password: %s + to: %s + from: %s + subject: %s`, configPath, dbg, serverAddress, @@ -193,21 +96,27 @@ func main() { subject, ) 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("Port", serverPort) - // config.Server.Merge("Encryption", encryption) - // config.Server.Merge("User", user) - // config.Server.Merge("Password", password) - config.Merge("Server", &ServerConfig{serverAddress, serverPort, encryption, user, password}) + config.Server.Merge("Address", serverAddress) + config.Server.Merge("Port", serverPort) + config.Server.Merge("Encryption", encryption) + config.Server.Merge("User", user) + config.Server.Merge("Password", password) config.Merge("From", from) - config.Merge("To", strings.Split(to, ",")) - config.Merge("Cc", strings.Split(cc, ",")) - config.Merge("Bcc", strings.Split(bcc, ",")) + config.Merge("To", toList(to)) + config.Merge("Cc", toList(cc)) + config.Merge("Bcc", toList(bcc)) 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) } diff --git a/test_conf.toml b/test_conf.toml index ed169d7..380e7d6 100644 --- a/test_conf.toml +++ b/test_conf.toml @@ -16,7 +16,7 @@ [default.server] address = "1.3.1.2" port = 587 - encryption = "tls" + encryption = true user = "faggiano@uccelli.net" password = "yougetit" @@ -38,7 +38,7 @@ [personal.server] address = "10.13.12.10" port = 25 - encryption = "starttls" + encryption = true user = "master@domain.local" password = "yougetitagain" @@ -46,5 +46,5 @@ [secret.server] address = "my.server.org" port = 12345 - encryption = "tls" + encryption = false user = "me" diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..8afaef0 --- /dev/null +++ b/utils.go @@ -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) + } + } +}