diff --git a/README.md b/README.md index dd1a208..f6feb64 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,9 @@ For authentication, you want to change your `/etc/dovecot/dovecot-sql.conf.ext` ``` driver = pgsql connect = host=YOURHOST port=5432 user=YOURUSER password=APASSWORD dbname=DBNAME -password_query = SELECT password, username AS user FROM users WHERE username = '%n' AND domain = '%d' -user_query = SELECT maildir, 1000 AS uid, 1000 AS gid FROM users WHERE username = '%n' AND domain = '%d' AND active = '1' + +password_query = SELECT concat(username, '@', domain) AS user, password FROM users WHERE username = '%n' AND domain = '%d' AND active = true +user_query = SELECT home, 1000 AS uid, 1000 AS gid FROM users WHERE username = '%n' AND domain = '%d' AND active = true ``` ### OpenDKIM @@ -60,6 +61,12 @@ postgres: user: postgres password: postgres123 sslmode: disable +defaults: + provider: ovh + # this is used to create the `home` field of the user. + # in systems with virtual mail this corresponds to the physical + # location of the vmail directory on your host + homeTemplate: "/var/lib/vmail/{{ .Domain }}/{{ .Username }}" providers: ovh: application_key: diff --git a/docker-compose.yml b/docker-compose.yml index a4628df..a3c0e92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,4 +12,4 @@ services: volumes: - postgres:/var/lib/postgresql/15/main volumes: - postgres: {} \ No newline at end of file + postgres: {} diff --git a/mailout.yml.sample b/mailout.yml.sample index 038cb20..445a136 100644 --- a/mailout.yml.sample +++ b/mailout.yml.sample @@ -5,7 +5,12 @@ postgres: user: postgres password: postgres123 sslmode: disable -defaultProvider: ovh +defaults: + provider: ovh + # this is used to create the `home` field of the user. + # in systems with virtual mail this corresponds to the physical + # location of the vmail directory on your host + homeTemplate: "/var/lib/vmail/{{ .Domain }}/{{ .Username }}" providers: ovh: application_key: diff --git a/pkg/cmd/dkimkey.go b/pkg/cmd/dkimkey.go index 1be46b7..caddd4a 100644 --- a/pkg/cmd/dkimkey.go +++ b/pkg/cmd/dkimkey.go @@ -27,10 +27,10 @@ var DKIMKeyCmd = &cobra.Command{ Short: "manages DKIM keys", PreRunE: func(cmd *cobra.Command, args []string) error { if flagProvider == "" { - if cfg.DefaultProvider == "" { + if cfg.Defaults.Provider == "" { logrus.Fatal("no provider specified and no default provider in config, aborting") } - flagProvider = cfg.DefaultProvider + flagProvider = cfg.Defaults.Provider } return nil @@ -240,7 +240,7 @@ var DKIMKeyPublishCmd = &cobra.Command{ pv := flagProvider if flagProvider == "" { - pv = cfg.DefaultProvider + pv = cfg.Defaults.Provider if pv == "" { logrus.Fatal("no provider specified") } @@ -299,7 +299,7 @@ var DKIMKeyUnpublishCmd = &cobra.Command{ pv := flagProvider if flagProvider == "" { - pv = cfg.DefaultProvider + pv = cfg.Defaults.Provider if pv == "" { logrus.Fatal("no provider specified") } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index bd5029b..4ce4124 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -30,6 +30,7 @@ var RootCmd = &cobra.Command{ func InitRootCmd() { InitUserCmd() InitDKIMKeyCmd() + InitTestCmd() RootCmd.AddCommand(VersionCmd) RootCmd.AddCommand(InitDBCmd) diff --git a/pkg/cmd/test.go b/pkg/cmd/test.go index 6d99200..f173ace 100644 --- a/pkg/cmd/test.go +++ b/pkg/cmd/test.go @@ -11,6 +11,11 @@ import ( "github.com/spf13/cobra" ) +var ( + flagTestMessage string + flagTestMessageSubject string +) + var TestCmd = &cobra.Command{ Use: "test", Short: "sends an email through the configured server", @@ -39,7 +44,7 @@ var TestCmd = &cobra.Command{ headers := make(map[string]string) headers["From"] = cfg.Test.Username headers["To"] = args[0] - headers["Subject"] = "This is a test email from the command line" + headers["Subject"] = flagTestMessageSubject message := "" for k, v := range headers { @@ -47,7 +52,7 @@ var TestCmd = &cobra.Command{ } message += "\r\n" - message += "This is a test email message sent through the command line utility." + message += flagTestMessage auth := smtp.PlainAuth("", cfg.Test.Username, cfg.Test.Password, strings.Split(cfg.Test.Address, ":")[0]) @@ -83,3 +88,8 @@ var TestCmd = &cobra.Command{ logrus.Info("sent test email") }, } + +func InitTestCmd() { + TestCmd.PersistentFlags().StringVarP(&flagTestMessage, "message", "m", "This is a test email message sent through the command line utility.", "What to send as a test message") + TestCmd.PersistentFlags().StringVarP(&flagTestMessageSubject, "subject", "s", "This is a test email from the command line", "What to send as a test message subject") +} diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index 4cb794e..dbd72f7 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -1,7 +1,9 @@ package cmd import ( + "bytes" "fmt" + "text/template" "git.maurice.fr/thomas/mailout/pkg/database" "git.maurice.fr/thomas/mailout/pkg/models" @@ -12,7 +14,12 @@ import ( ) var ( - flagUserActive bool + flagUserActive bool + flagUserHome string + flagUserQuota int64 + flagUserPassword string + flagUserGID int + flagUserUID int ) var UserCmd = &cobra.Command{ @@ -73,6 +80,24 @@ var UserAddCmd = &cobra.Command{ Active: flagUserActive, Password: fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash)), } + + if flagUserHome == "" { + flagUserHome = cfg.Defaults.HomeTemplate + } + + tmpl, err := template.New("").Parse(flagUserHome) + if err != nil { + logrus.WithError(err).Fatal("could not parse the default home template") + } + + buf := bytes.NewBufferString("") + err = tmpl.Execute(buf, user) + if err != nil { + logrus.WithError(err).Fatal("could not render template") + } + + user.Home = buf.String() + err = db.Save(&user).Error if err != nil { @@ -100,10 +125,10 @@ var UserListCmd = &cobra.Command{ } tData := pterm.TableData{ - {"id", "username", "domain", "active", "uid", "gid", "created_at", "updated_at"}, + {"id", "username", "domain", "active", "uid", "gid", "home"}, } for _, u := range qRes { - tData = append(tData, []string{u.ID.String(), u.Username, u.Domain, fmt.Sprintf("%v", u.Active), fmt.Sprintf("%d", u.UID), fmt.Sprintf("%d", u.GID), u.CreatedAt.String(), u.UpdatedAt.String()}) + tData = append(tData, []string{u.ID.String(), u.Username, u.Domain, fmt.Sprintf("%v", u.Active), fmt.Sprintf("%d", u.UID), fmt.Sprintf("%d", u.GID), u.Home}) } pterm.DefaultTable.WithHasHeader().WithData(tData).Render() @@ -182,12 +207,90 @@ var UserDeactivateCmd = &cobra.Command{ }, } +var UserEditCmd = &cobra.Command{ + Use: "edit", + Short: "edites a user", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + db, err := database.NewDB(cfg) + if err != nil { + logrus.WithError(err).Fatal("could not connect to the database") + } + + userQuery, err := buildUserQuery(args[0]) + if err != nil { + logrus.WithError(err).Fatal("unable to determine user") + } + + qRes := make([]models.User, 0) + + err = db.Model(&models.User{}).Find(&qRes, userQuery).Error + if err != nil { + logrus.WithError(err).Fatal("could not preload user") + } + + if len(qRes) == 0 { + logrus.Fatal("no such user") + } + + user := qRes[0] + + if flagUserHome != "" { + tmpl, err := template.New("").Parse(flagUserHome) + if err != nil { + logrus.WithError(err).Fatal("could not parse the default home template") + } + + buf := bytes.NewBufferString("") + err = tmpl.Execute(buf, user) + if err != nil { + logrus.WithError(err).Fatal("could not render home template") + } + + user.Home = buf.String() + } + + if flagUserPassword != "" { + passwordHash, err := bcrypt.GenerateFromPassword([]byte(args[1]), bcrypt.DefaultCost) + if err != nil { + logrus.WithError(err).Fatal("could not compute user password hash") + } + + user.Password = fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash)) + } + + if flagUserGID != -1 { + user.GID = flagUserGID + } + + if flagUserUID != -1 { + user.UID = flagUserUID + } + + err = db.Save(&user).Error + + if err != nil { + logrus.WithError(err).Fatal("could not update user") + } + + logrus.Infof("edited user %s", args[0]) + }, +} + func InitUserCmd() { UserCmd.AddCommand(UserAddCmd) + UserCmd.AddCommand(UserEditCmd) UserCmd.AddCommand(UserListCmd) UserCmd.AddCommand(UserDeleteCmd) UserCmd.AddCommand(UserActivateCmd) UserCmd.AddCommand(UserDeactivateCmd) UserAddCmd.PersistentFlags().BoolVarP(&flagUserActive, "active", "a", true, "whether or not the created user is active") + UserAddCmd.PersistentFlags().StringVarP(&flagUserHome, "home", "", "", "template to use for user's home directories") + + UserEditCmd.PersistentFlags().StringVarP(&flagUserPassword, "password", "p", "", "User password") + UserEditCmd.PersistentFlags().StringVarP(&flagUserHome, "home", "", "", "home (user's mailbox)") + UserEditCmd.PersistentFlags().Int64VarP(&flagUserQuota, "quota", "q", -1, "Quota in bytes for the user") + UserEditCmd.PersistentFlags().IntVarP(&flagUserUID, "uid", "u", -1, "user's uid") + UserEditCmd.PersistentFlags().IntVarP(&flagUserGID, "gid", "g", -1, "user's gid") } diff --git a/pkg/config/config.go b/pkg/config/config.go index a282700..eccb9c0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,8 +16,11 @@ type Config struct { Database string `yaml:"database"` SSLMode string `yaml:"sslmode"` } `yaml:"postgres"` - DefaultProvider string `yaml:"defaultProvider"` - Providers struct { + Defaults struct { + HomeTemplate string `yaml:"homeTemplate"` + Provider string `yaml:"provider"` + } `yaml:"defaults"` + Providers struct { OVH *providerConfigs.OVHConfig `yaml:"ovh"` } `yaml:"providers"` Test *struct {