diff --git a/config.go b/config.go index 9f946ad..f5ba763 100644 --- a/config.go +++ b/config.go @@ -7,20 +7,51 @@ import ( "os" ) +type MailSendConfig struct { + From string `json:"from"` + To string `json:"to"` +} + +type SMTPConfig struct { + Host string `json:"host"` + Port string `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + Insecure bool `json:"insecure"` + Mails []MailSendConfig `json:"mails"` +} + type Config struct { - BaseURL string `json:"baseurl"` - CertFingerprint string `json:"certfingerprint"` - AuthID string `json:"authid"` - Secret string `json:"secret"` - Datastore string `json:"datastore"` - Namespace string `json:"namespace"` - BackupID string `json:"backup-id"` - BackupSourceDir string `json:"backupdir"` - PxarOut string `json:"pxarout"` + BaseURL string `json:"baseurl"` + CertFingerprint string `json:"certfingerprint"` + AuthID string `json:"authid"` + Secret string `json:"secret"` + Datastore string `json:"datastore"` + Namespace string `json:"namespace"` + BackupID string `json:"backup-id"` + BackupSourceDir string `json:"backupdir"` + PxarOut string `json:"pxarout"` + SMTP *SMTPConfig `json:"smtp"` } func (c *Config) valid() bool { - return c.BaseURL != "" && c.CertFingerprint != "" && c.AuthID != "" && c.Secret != "" && c.Datastore != "" && c.BackupSourceDir != "" + baseValid := c.BaseURL != "" && c.CertFingerprint != "" && c.AuthID != "" && c.Secret != "" && c.Datastore != "" && c.BackupSourceDir != "" + if !baseValid { + return baseValid + } + + if c.SMTP != nil { + mailCfgValid := c.SMTP.Host != "" && c.SMTP.Port != "" && c.SMTP.Username != "" && c.SMTP.Password != "" + if len(c.SMTP.Mails) == 0 { + return false + } + for i := range c.SMTP.Mails { + mailCfgValid = mailCfgValid && (c.SMTP.Mails[i].From != "" && c.SMTP.Mails[i].To != "") + } + return mailCfgValid + } + + return true } func loadConfig() *Config { @@ -34,12 +65,20 @@ func loadConfig() *Config { backupIDFlag := flag.String("backup-id", "", "Backup ID (optional - if not specified, the hostname is used as the default)") backupSourceDirFlag := flag.String("backupdir", "", "Backup source directory, must not be symlink") pxarOutFlag := flag.String("pxarout", "", "Output PXAR archive for debug purposes (optional)") - configFile := flag.String("config", "", "Path to JSON config file") + + mailHostFlag := flag.String("mail-host", "", "mail notification system: mail server host(optional)") + mailPortFlag := flag.String("mail-port", "", "mail notification system: mail server port(optional)") + mailUsernameFlag := flag.String("mail-username", "", "mail notification system: mail server username(optional)") + mailPasswordFlag := flag.String("mail-password", "", "mail notification system: mail server password(optional)") + mailInsecureFlag := flag.Bool("mail-insecure", false, "mail notification system: allow insecure communications(optional)") + mailFromFlag := flag.String("mail-from", "", "mail notification system: sender mail(optional)") + mailToFlag := flag.String("mail-to", "", "mail notification system: receiver mail(optional)") + + configFile := flag.String("config", "", "Path to JSON config file. If this flag is provided all the others are ignored") // Parse command line flags flag.Parse() - // Create a config struct and try to load values from the JSON file if specified config := &Config{} if *configFile != "" { file, err := os.ReadFile(*configFile) @@ -52,35 +91,59 @@ func loadConfig() *Config { fmt.Printf("Error parsing config file: %v\n", err) os.Exit(1) } + + return config } - // Override JSON config with command line flags if provided - if *baseURLFlag != "" { - config.BaseURL = *baseURLFlag + config.BaseURL = *baseURLFlag + config.CertFingerprint = *certFingerprintFlag + config.AuthID = *authIDFlag + config.Secret = *secretFlag + config.Datastore = *datastoreFlag + config.Namespace = *namespaceFlag + config.BackupID = *backupIDFlag + config.BackupSourceDir = *backupSourceDirFlag + config.PxarOut = *pxarOutFlag + + initSmtpConfigIfNeeded := func() { + if config.SMTP == nil { + config.SMTP = &SMTPConfig{} + } } - if *certFingerprintFlag != "" { - config.CertFingerprint = *certFingerprintFlag + initMailConfsIfNeeded := func() { + initSmtpConfigIfNeeded() + if len(config.SMTP.Mails) == 0 { + config.SMTP.Mails = append(config.SMTP.Mails, MailSendConfig{}) + } } - if *authIDFlag != "" { - config.AuthID = *authIDFlag + + if *mailHostFlag != "" { + initSmtpConfigIfNeeded() + config.SMTP.Host = *mailHostFlag } - if *secretFlag != "" { - config.Secret = *secretFlag + if *mailPortFlag != "" { + initSmtpConfigIfNeeded() + config.SMTP.Port = *mailPortFlag } - if *datastoreFlag != "" { - config.Datastore = *datastoreFlag + if *mailUsernameFlag != "" { + initSmtpConfigIfNeeded() + config.SMTP.Username = *mailUsernameFlag } - if *namespaceFlag != "" { - config.Namespace = *namespaceFlag + if *mailPasswordFlag != "" { + initSmtpConfigIfNeeded() + config.SMTP.Password = *mailPasswordFlag } - if *backupIDFlag != "" { - config.BackupID = *backupIDFlag + if *mailInsecureFlag { + initSmtpConfigIfNeeded() + config.SMTP.Insecure = *mailInsecureFlag } - if *backupSourceDirFlag != "" { - config.BackupSourceDir = *backupSourceDirFlag + if *mailFromFlag != "" { + initMailConfsIfNeeded() + config.SMTP.Mails[0].From = *mailFromFlag } - if *pxarOutFlag != "" { - config.PxarOut = *pxarOutFlag + if *mailToFlag != "" { + initMailConfsIfNeeded() + config.SMTP.Mails[0].To = *mailToFlag } return config diff --git a/config.json.example b/config.json.example index fb1b3d6..7250583 100644 --- a/config.json.example +++ b/config.json.example @@ -7,5 +7,19 @@ "backupdir": "C:", "namespace": "", "backup-id": "", - "pxarout": "" + "pxarout": "", + "smtp": { + "host": "smtp.example.com", + "port": "465", + "username": "my-user@example.com", + "password": "my-password", + "mails": [{ + "from": "sender1@example.com", + "to": "receiver1@example.com + }, { + "from": "sender2@example.com", + "to": "receiver2@example.com + }] + + } } diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..5a599d6 --- /dev/null +++ b/mail.go @@ -0,0 +1,115 @@ +package main + +import ( + "crypto/tls" + "errors" + "fmt" + "net/smtp" +) + +type unencryptedAuth struct { + smtp.Auth +} + +func (a unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + s := *server + s.TLS = true + return a.Auth.Start(&s) +} + +func setupClient(host, port, username, password string, allowInsecure bool) (*smtp.Client, error) { + var auth smtp.Auth + auth = smtp.PlainAuth("", username, password, host) + if port == "25" { + if !allowInsecure { + return nil, errors.New("sending plain password over unencrypted connection") + } + auth = unencryptedAuth{auth} + } + + var tlsconfig *tls.Config + if port != "25" { + // TLS config + tlsconfig = &tls.Config{ + InsecureSkipVerify: allowInsecure, + ServerName: host, + } + } + + servername := host + ":" + port + + var c *smtp.Client + var err error + if port == "465" { + // Here is the key, you need to call tls.Dial instead of smtp.Dial + // for smtp servers running on 465 that require an ssl connection + // from the very beginning (no starttls) + conn, err := tls.Dial("tcp", servername, tlsconfig) + if err != nil { + return nil, err + } + + c, err = smtp.NewClient(conn, host) + if err != nil { + return nil, err + } + } else { + c, err = smtp.Dial(servername) + if err != nil { + return nil, err + } + if port == "587" { + c.StartTLS(tlsconfig) + } + } + + // Auth + if err = c.Auth(auth); err != nil { + fmt.Println("here", err) + return nil, err + } + + return c, nil +} + +func sendMail(from, to, subject, body string, c *smtp.Client) error { + // Setup headers + headers := make(map[string]string) + headers["From"] = from + headers["To"] = to + headers["Subject"] = subject + + // Setup message + message := "" + for k, v := range headers { + message += fmt.Sprintf("%s: %s\r\n", k, v) + } + message += "\r\n" + body + + // To && From + if err := c.Mail(from); err != nil { + return err + } + + if err := c.Rcpt(to); err != nil { + return err + } + + // Data + w, err := c.Data() + if err != nil { + return err + } + + _, err = w.Write([]byte(message)) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return nil +} diff --git a/main.go b/main.go index bfbed8e..88e5b4e 100644 --- a/main.go +++ b/main.go @@ -73,7 +73,7 @@ func main() { go systray.Run(func() { systray.SetIcon(ICON) systray.SetTooltip("PBSGO Backup running") - beeep.Notify("Proxmox Backup Go", fmt.Sprintf("Backup started"), "") + beeep.Notify("Proxmox Backup Go", "Backup started", "") }, func() { @@ -105,6 +105,27 @@ func main() { systray.Quit() beeep.Notify("Proxmox Backup Go", msg, "") } + if cfg.SMTP != nil { + var subject string + if err == nil { + subject = "Backup complete" + } else { + subject = "Backup error" + } + client, err := setupClient(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.Insecure) + if err != nil { + fmt.Println("Cannot connect to mail server: " + err.Error()) + os.Exit(1) + } + defer client.Quit() + for _, ccc := range cfg.SMTP.Mails { + err = sendMail(ccc.From, ccc.To, subject, msg, client) + if err != nil { + fmt.Println("Cannot send email: " + err.Error()) + os.Exit(1) + } + } + } }