Browse Source

Refactor in multiple files. First working version.

Blallo 4 years ago
parent
commit
02ab2ec867
8 changed files with 425 additions and 162 deletions
  1. 126 0
      config.go
  2. 10 0
      go.mod
  3. 24 0
      go.sum
  4. 96 0
      log.go
  5. 45 0
      mail.go
  6. 68 159
      main.go
  7. 3 3
      test_conf.toml
  8. 53 0
      utils.go

+ 126 - 0
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
+}

+ 10 - 0
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
+)

+ 24 - 0
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=

+ 96 - 0
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)
+}

+ 45 - 0
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)
+	}
+}

+ 68 - 159
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
-}
-
-func (c *Config) Validate() error {
-	return nil
+	return text
 }
 
-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)
 }

+ 3 - 3
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"

+ 53 - 0
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)
+		}
+	}
+}