Compare commits

..

No commits in common. "master" and "v0.0.2" have entirely different histories.

8 changed files with 19 additions and 145 deletions

View file

@ -17,6 +17,9 @@ The plan is basically:
* you have one for the DKIM keys you use * you have one for the DKIM keys you use
* you have a view to expose the relevant key to use for opendkim * you have a view to expose the relevant key to use for opendkim
## Warning
I halfed assed that shit on an afternoon to suit my own needs, improvements will follow, or not.
## Features ## Features
* Create mail users * Create mail users
@ -37,9 +40,8 @@ For authentication, you want to change your `/etc/dovecot/dovecot-sql.conf.ext`
``` ```
driver = pgsql driver = pgsql
connect = host=YOURHOST port=5432 user=YOURUSER password=APASSWORD dbname=DBNAME 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'
password_query = SELECT concat(username, '@', domain) AS user, password FROM users WHERE username = '%n' AND domain = '%d' AND active = true user_query = SELECT maildir, 1000 AS uid, 1000 AS gid FROM users WHERE username = '%n' AND domain = '%d' AND active = '1'
user_query = SELECT home, 1000 AS uid, 1000 AS gid FROM users WHERE username = '%n' AND domain = '%d' AND active = true
``` ```
### OpenDKIM ### OpenDKIM
@ -58,12 +60,6 @@ postgres:
user: postgres user: postgres
password: postgres123 password: postgres123
sslmode: disable 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: providers:
ovh: ovh:
application_key: <CHANGEME> application_key: <CHANGEME>
@ -72,4 +68,4 @@ providers:
endpoint: ovh-eu endpoint: ovh-eu
``` ```
pointing to your DB/OVH account and you're good pointing to your DB/OVH account and you're good

View file

@ -12,4 +12,4 @@ services:
volumes: volumes:
- postgres:/var/lib/postgresql/15/main - postgres:/var/lib/postgresql/15/main
volumes: volumes:
postgres: {} postgres: {}

View file

@ -5,12 +5,7 @@ postgres:
user: postgres user: postgres
password: postgres123 password: postgres123
sslmode: disable sslmode: disable
defaults: defaultProvider: ovh
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: providers:
ovh: ovh:
application_key: <CHANGEME> application_key: <CHANGEME>

View file

@ -27,10 +27,10 @@ var DKIMKeyCmd = &cobra.Command{
Short: "manages DKIM keys", Short: "manages DKIM keys",
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
if flagProvider == "" { if flagProvider == "" {
if cfg.Defaults.Provider == "" { if cfg.DefaultProvider == "" {
logrus.Fatal("no provider specified and no default provider in config, aborting") logrus.Fatal("no provider specified and no default provider in config, aborting")
} }
flagProvider = cfg.Defaults.Provider flagProvider = cfg.DefaultProvider
} }
return nil return nil
@ -240,7 +240,7 @@ var DKIMKeyPublishCmd = &cobra.Command{
pv := flagProvider pv := flagProvider
if flagProvider == "" { if flagProvider == "" {
pv = cfg.Defaults.Provider pv = cfg.DefaultProvider
if pv == "" { if pv == "" {
logrus.Fatal("no provider specified") logrus.Fatal("no provider specified")
} }
@ -299,7 +299,7 @@ var DKIMKeyUnpublishCmd = &cobra.Command{
pv := flagProvider pv := flagProvider
if flagProvider == "" { if flagProvider == "" {
pv = cfg.Defaults.Provider pv = cfg.DefaultProvider
if pv == "" { if pv == "" {
logrus.Fatal("no provider specified") logrus.Fatal("no provider specified")
} }

View file

@ -30,7 +30,6 @@ var RootCmd = &cobra.Command{
func InitRootCmd() { func InitRootCmd() {
InitUserCmd() InitUserCmd()
InitDKIMKeyCmd() InitDKIMKeyCmd()
InitTestCmd()
RootCmd.AddCommand(VersionCmd) RootCmd.AddCommand(VersionCmd)
RootCmd.AddCommand(InitDBCmd) RootCmd.AddCommand(InitDBCmd)

View file

@ -11,11 +11,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var (
flagTestMessage string
flagTestMessageSubject string
)
var TestCmd = &cobra.Command{ var TestCmd = &cobra.Command{
Use: "test", Use: "test",
Short: "sends an email through the configured server", Short: "sends an email through the configured server",
@ -44,7 +39,7 @@ var TestCmd = &cobra.Command{
headers := make(map[string]string) headers := make(map[string]string)
headers["From"] = cfg.Test.Username headers["From"] = cfg.Test.Username
headers["To"] = args[0] headers["To"] = args[0]
headers["Subject"] = flagTestMessageSubject headers["Subject"] = "This is a test email from the command line"
message := "" message := ""
for k, v := range headers { for k, v := range headers {
@ -52,7 +47,7 @@ var TestCmd = &cobra.Command{
} }
message += "\r\n" message += "\r\n"
message += flagTestMessage message += "This is a test email message sent through the command line utility."
auth := smtp.PlainAuth("", cfg.Test.Username, cfg.Test.Password, strings.Split(cfg.Test.Address, ":")[0]) auth := smtp.PlainAuth("", cfg.Test.Username, cfg.Test.Password, strings.Split(cfg.Test.Address, ":")[0])
@ -88,8 +83,3 @@ var TestCmd = &cobra.Command{
logrus.Info("sent test email") 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")
}

View file

@ -1,9 +1,7 @@
package cmd package cmd
import ( import (
"bytes"
"fmt" "fmt"
"text/template"
"git.maurice.fr/thomas/mailout/pkg/database" "git.maurice.fr/thomas/mailout/pkg/database"
"git.maurice.fr/thomas/mailout/pkg/models" "git.maurice.fr/thomas/mailout/pkg/models"
@ -14,12 +12,7 @@ import (
) )
var ( var (
flagUserActive bool flagUserActive bool
flagUserHome string
flagUserQuota int64
flagUserPassword string
flagUserGID int
flagUserUID int
) )
var UserCmd = &cobra.Command{ var UserCmd = &cobra.Command{
@ -80,24 +73,6 @@ var UserAddCmd = &cobra.Command{
Active: flagUserActive, Active: flagUserActive,
Password: fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash)), 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 err = db.Save(&user).Error
if err != nil { if err != nil {
@ -125,10 +100,10 @@ var UserListCmd = &cobra.Command{
} }
tData := pterm.TableData{ tData := pterm.TableData{
{"id", "username", "domain", "active", "uid", "gid", "home"}, {"id", "username", "domain", "active", "uid", "gid", "created_at", "updated_at"},
} }
for _, u := range qRes { 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.Home}) 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()})
} }
pterm.DefaultTable.WithHasHeader().WithData(tData).Render() pterm.DefaultTable.WithHasHeader().WithData(tData).Render()
@ -207,90 +182,12 @@ var UserDeactivateCmd = &cobra.Command{
}, },
} }
var UserEditCmd = &cobra.Command{
Use: "edit <user>",
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(flagUserPassword), 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() { func InitUserCmd() {
UserCmd.AddCommand(UserAddCmd) UserCmd.AddCommand(UserAddCmd)
UserCmd.AddCommand(UserEditCmd)
UserCmd.AddCommand(UserListCmd) UserCmd.AddCommand(UserListCmd)
UserCmd.AddCommand(UserDeleteCmd) UserCmd.AddCommand(UserDeleteCmd)
UserCmd.AddCommand(UserActivateCmd) UserCmd.AddCommand(UserActivateCmd)
UserCmd.AddCommand(UserDeactivateCmd) UserCmd.AddCommand(UserDeactivateCmd)
UserAddCmd.PersistentFlags().BoolVarP(&flagUserActive, "active", "a", true, "whether or not the created user is active") 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")
} }

View file

@ -16,11 +16,8 @@ type Config struct {
Database string `yaml:"database"` Database string `yaml:"database"`
SSLMode string `yaml:"sslmode"` SSLMode string `yaml:"sslmode"`
} `yaml:"postgres"` } `yaml:"postgres"`
Defaults struct { DefaultProvider string `yaml:"defaultProvider"`
HomeTemplate string `yaml:"homeTemplate"` Providers struct {
Provider string `yaml:"provider"`
} `yaml:"defaults"`
Providers struct {
OVH *providerConfigs.OVHConfig `yaml:"ovh"` OVH *providerConfigs.OVHConfig `yaml:"ovh"`
} `yaml:"providers"` } `yaml:"providers"`
Test *struct { Test *struct {