feat(init): First commit

This commit is contained in:
Thomas Maurice 2024-02-11 19:59:34 +01:00
commit f92368748a
Signed by: thomas
GPG key ID: 1D577F50583032A6
22 changed files with 1298 additions and 0 deletions

417
pkg/cmd/dkimkey.go Normal file
View file

@ -0,0 +1,417 @@
package cmd
import (
"crypto/rand"
"encoding/hex"
"fmt"
"git.maurice.fr/thomas/mailout/pkg/crypto"
"git.maurice.fr/thomas/mailout/pkg/database"
"git.maurice.fr/thomas/mailout/pkg/models"
"git.maurice.fr/thomas/mailout/pkg/providers/ovh"
"git.maurice.fr/thomas/mailout/pkg/utils"
"github.com/google/uuid"
"github.com/pterm/pterm"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
flagDKIMKeyActive bool
flagSelector string
flagKeyBits int
)
var DKIMKeyCmd = &cobra.Command{
Use: "dkimkey",
Short: "manages DKIM keys",
}
var DKIMKeyAddCmd = &cobra.Command{
Use: "add [domain]",
Short: "adds a DKIM signing key",
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")
}
priv, pub, err := crypto.GenerateKeyPair(flagKeyBits)
if err != nil {
panic(err)
}
selector := flagSelector
if selector == "" {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
logrus.Fatal("entropy generator is kill")
}
selector = hex.EncodeToString(b)
}
dkimkey := models.DKIMKey{
DomainName: args[0],
PublicKey: pub,
PrivateKey: priv,
Active: flagUserActive,
Selector: selector,
Record: fmt.Sprintf("%s._domainkey.%s. IN TXT \"v=DKIM1; k=rsa; p=%s\"", selector, args[0], pub),
}
err = db.Save(&dkimkey).Error
if err != nil {
logrus.WithError(err).Fatal("could not create DKIM key")
}
logrus.Infof("created DKIM key %s", dkimkey.ID)
},
}
var DKIMKeyListCmd = &cobra.Command{
Use: "list",
Short: "list DKIM keys",
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")
}
qRes := make([]models.DKIMKey, 0)
err = db.Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not list keys")
}
tData := pterm.TableData{
{"id", "selector", "domain", "active", "created_at", "updated_at"},
}
for _, k := range qRes {
tData = append(tData, []string{k.ID.String(), k.Selector, k.DomainName, fmt.Sprintf("%v", k.Active), k.CreatedAt.String(), k.UpdatedAt.String()})
}
pterm.DefaultTable.WithHasHeader().WithData(tData).Render()
},
}
var DKIMKeyRecordCmd = &cobra.Command{
Use: "record",
Short: "gives the dns record for a key",
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")
}
qRes := make([]models.DKIMKey, 0)
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not search keys")
}
if len(qRes) == 0 {
logrus.WithError(err).Fatal("No such key")
}
fmt.Println(qRes[0].Record)
},
}
var DKIMKeyDeleteCmd = &cobra.Command{
Use: "delete",
Short: "delete DKIM keys",
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")
}
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(&models.DKIMKey{ID: id}).Delete(&models.DKIMKey{}).Error
if err != nil {
logrus.WithError(err).Fatal("could not delete DKIM key")
}
logrus.Infof("deleted DKIM key %s if it existed", args[0])
},
}
var DKIMKeyActivateCmd = &cobra.Command{
Use: "activate",
Short: "activates a DKIM key",
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")
}
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Model(&models.DKIMKey{}).Where(&models.DKIMKey{ID: id}).Update("active", true).Error
if err != nil {
logrus.WithError(err).Fatal("could not activate DKIM key")
}
logrus.Infof("activated DKIM key %s", args[0])
},
}
var DKIMKeyDeactivateCmd = &cobra.Command{
Use: "deactivate",
Short: "deactivates a DKIM key",
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")
}
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Model(&models.DKIMKey{}).Where(&models.DKIMKey{ID: id}).Update("active", false).Error
if err != nil {
logrus.WithError(err).Fatal("could not deactivate DKIM key")
}
logrus.Infof("deactivated DKIM key %s", args[0])
},
}
var DKIMKeyPublishCmd = &cobra.Command{
Use: "publish",
Short: "publishes a DKIM key onto a DNS provider",
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")
}
qRes := make([]models.DKIMKey, 0)
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not get key")
}
if len(qRes) == 0 {
logrus.WithError(err).Fatal("No such key")
}
dkimkey := qRes[0]
logger := logrus.WithFields(logrus.Fields{
"dkimkey": dkimkey.ID,
"selector": dkimkey.Selector,
})
ovhClient, err := ovh.NewOVHProvider(cfg)
if err != nil {
logger.WithError(err).Fatal("could not get OVH DNS provider")
}
zone, err := utils.GetZone(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine zone")
}
subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine subdomain")
}
if subdomain == "" {
subdomain = zone
}
logger = logger.WithFields(logrus.Fields{
"zone": zone,
"subdomain": subdomain,
})
dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
result := make([]int, 0)
err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
if err != nil {
logger.WithError(err).Fatal("could not lookup records")
}
type createParams struct {
FieldType string `json:"fieldType"`
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
}
type updateParams struct {
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
}
if len(result) == 0 {
logger.Info("no DKIM records found, creating a new one")
c := createParams{
FieldType: "TXT",
SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
Target: fmt.Sprintf("\"v=DKIM1; k=rsa; p=%s\"", dkimkey.PublicKey),
TTL: 60,
}
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/record", zone), &c, nil)
if err != nil {
logger.WithError(err).Fatal("could not create new record")
}
logger.Info("created new DKIM record")
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
if err != nil {
logger.WithError(err).Fatal("could not refresh the zone")
}
logger.Info("refreshed zone")
} else if len(result) == 1 {
logger.Info("found one record, updating it")
u := updateParams{
SubDomain: fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain),
Target: fmt.Sprintf("\"v=DKIM1; k=foo; p=%s\"", dkimkey.PublicKey),
TTL: 60,
}
err = ovhClient.Client.Put(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), &u, nil)
if err != nil {
logger.WithError(err).Fatal("could not update record")
}
logger.Info("updated existing record")
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
if err != nil {
logger.WithError(err).Fatal("could not refresh the zone")
}
logger.Info("refreshed zone")
}
},
}
var DKIMKeyUnpublishCmd = &cobra.Command{
Use: "unpublish",
Short: "removes a DKIM key onto a DNS provider",
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")
}
qRes := make([]models.DKIMKey, 0)
id, err := uuid.Parse(args[0])
if err != nil {
logrus.WithError(err).Fatal("invalid UUID")
}
err = db.Where(models.DKIMKey{ID: id}).Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not get key")
}
if len(qRes) == 0 {
logrus.WithError(err).Fatal("No such key")
}
dkimkey := qRes[0]
logger := logrus.WithFields(logrus.Fields{
"dkimkey": dkimkey.ID,
"selector": dkimkey.Selector,
})
ovhClient, err := ovh.NewOVHProvider(cfg)
if err != nil {
logger.WithError(err).Fatal("could not get OVH DNS provider")
}
zone, err := utils.GetZone(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine zone")
}
subdomain, err := utils.GetSubdomain(dkimkey.DomainName)
if err != nil {
logger.WithError(err).Fatal("could not determine subdomain")
}
if subdomain == "" {
subdomain = zone
}
logger = logger.WithFields(logrus.Fields{
"zone": zone,
"subdomain": subdomain,
})
dkimSub := fmt.Sprintf("%s._domainkey.%s", dkimkey.Selector, subdomain)
result := make([]int, 0)
err = ovhClient.Client.Get(fmt.Sprintf("/domain/zone/%s/record?fieldType=TXT&subDomain=%s", zone, dkimSub), &result)
if err != nil {
logger.WithError(err).Fatal("could not lookup records")
}
if len(result) == 0 {
logger.Info("no DKIM records found, no need to do anything")
} else if len(result) == 1 {
logger.Info("found one record, deleting it")
err = ovhClient.Client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", zone, result[0]), nil)
if err != nil {
logger.WithError(err).Fatal("could not delete record")
}
logger.Info("deleted existing record")
err = ovhClient.Client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil)
if err != nil {
logger.WithError(err).Fatal("could not refresh the zone")
}
logger.Info("refreshed zone")
}
},
}
func InitDKIMKeyCmd() {
DKIMKeyCmd.AddCommand(DKIMKeyAddCmd)
DKIMKeyCmd.AddCommand(DKIMKeyListCmd)
DKIMKeyCmd.AddCommand(DKIMKeyDeleteCmd)
DKIMKeyCmd.AddCommand(DKIMKeyActivateCmd)
DKIMKeyCmd.AddCommand(DKIMKeyDeactivateCmd)
DKIMKeyCmd.AddCommand(DKIMKeyRecordCmd)
DKIMKeyCmd.AddCommand(DKIMKeyPublishCmd)
DKIMKeyCmd.AddCommand(DKIMKeyUnpublishCmd)
DKIMKeyAddCmd.PersistentFlags().BoolVarP(&flagDKIMKeyActive, "active", "a", true, "whether or not the created key is active")
DKIMKeyAddCmd.PersistentFlags().StringVarP(&flagSelector, "selector", "s", "", "force a selector for the key")
DKIMKeyAddCmd.PersistentFlags().IntVarP(&flagKeyBits, "key-bits", "k", 2048, "force a size for the key")
}

25
pkg/cmd/initdb.go Normal file
View file

@ -0,0 +1,25 @@
package cmd
import (
"git.maurice.fr/thomas/mailout/pkg/database"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var InitDBCmd = &cobra.Command{
Use: "initdb",
Short: "initialises the database",
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")
}
err = database.InitMigrate(db)
if err != nil {
logrus.WithError(err).Fatal("could not initialise the database")
}
logrus.Info("successfully initialised the database")
},
}

48
pkg/cmd/root.go Normal file
View file

@ -0,0 +1,48 @@
package cmd
import (
"fmt"
"git.maurice.fr/thomas/mailout/pkg/config"
"github.com/spf13/cobra"
)
var (
configFile string
cfg *config.Config
)
var RootCmd = &cobra.Command{
Use: "mailoutctl",
Short: "mailout management utility",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error
cfg, err = config.LoadConfig(configFile)
if err != nil {
return err
}
return nil
},
}
var VersionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("v0.0.1")
},
}
func InitRootCmd() {
InitUserCmd()
InitDKIMKeyCmd()
RootCmd.AddCommand(VersionCmd)
RootCmd.AddCommand(InitDBCmd)
RootCmd.AddCommand(UserCmd)
RootCmd.AddCommand(DKIMKeyCmd)
RootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "mailout.yml", "Configuration file")
}

193
pkg/cmd/user.go Normal file
View file

@ -0,0 +1,193 @@
package cmd
import (
"fmt"
"git.maurice.fr/thomas/mailout/pkg/database"
"git.maurice.fr/thomas/mailout/pkg/models"
"github.com/pterm/pterm"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
)
var (
flagUserActive bool
)
var UserCmd = &cobra.Command{
Use: "user",
Short: "manages users",
}
var UserAddCmd = &cobra.Command{
Use: "add [user_address] [password]",
Short: "adds a user",
Args: cobra.ExactArgs(2),
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")
}
username, domain, err := splitUser(args[0])
if err != nil {
logrus.WithError(err).Fatal("could not parse user")
}
qRes := make([]models.User, 0)
var user models.User
userExists := false
logger := logrus.WithField("user", args[0])
err = db.Where(&models.User{Domain: domain, Username: username}).Find(&qRes).Error
if err != nil {
logger.WithError(err).Fatal("could not check user's existence")
}
if len(qRes) == 0 {
userExists = false
} else {
logger.Warning("user already exists, it's password will be updated")
user = qRes[0]
userExists = true
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(args[1]), bcrypt.DefaultCost)
if err != nil {
logger.WithError(err).Fatal("could not compute user password hash")
}
if userExists {
err = db.Model(&models.User{}).Where(&user).Update("password", fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash))).Error
if err != nil {
logger.WithError(err).Fatal("could not update user password")
}
logger.Infof("updated user %s", user.ID)
return
}
user = models.User{
Username: username,
Domain: domain,
Active: flagUserActive,
Password: fmt.Sprintf("{BLF-CRYPT}%s", string(passwordHash)),
}
err = db.Save(&user).Error
if err != nil {
logger.WithError(err).Fatal("could not create user")
}
logger.Infof("created user %s", user.ID)
},
}
var UserListCmd = &cobra.Command{
Use: "list",
Short: "list users",
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")
}
qRes := make([]models.User, 0)
err = db.Find(&qRes).Error
if err != nil {
logrus.WithError(err).Fatal("could not list users")
}
tData := pterm.TableData{
{"id", "username", "domain", "active", "uid", "gid", "created_at", "updated_at"},
}
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()})
}
pterm.DefaultTable.WithHasHeader().WithData(tData).Render()
},
}
var UserDeleteCmd = &cobra.Command{
Use: "delete",
Short: "delete users",
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")
}
err = db.Where(&userQuery).Delete(&models.User{}).Error
if err != nil {
logrus.WithError(err).Fatal("could not delete user")
}
logrus.Infof("deleted user %s if it existed", args[0])
},
}
var UserActivateCmd = &cobra.Command{
Use: "activate",
Short: "activates 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")
}
err = db.Model(&models.User{}).Where(&userQuery).Update("active", true).Error
if err != nil {
logrus.WithError(err).Fatal("could not activate user")
}
logrus.Infof("activated user %s", args[0])
},
}
var UserDeactivateCmd = &cobra.Command{
Use: "deactivate",
Short: "deactivates 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")
}
err = db.Model(&models.User{}).Where(&userQuery).Update("active", false).Error
if err != nil {
logrus.WithError(err).Fatal("could not deactivate user")
}
logrus.Infof("deactivated user %s", args[0])
},
}
func InitUserCmd() {
UserCmd.AddCommand(UserAddCmd)
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")
}

45
pkg/cmd/utils.go Normal file
View file

@ -0,0 +1,45 @@
package cmd
import (
"fmt"
"regexp"
"strings"
"git.maurice.fr/thomas/mailout/pkg/models"
"github.com/google/uuid"
)
var (
uuidRegex = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
)
func splitUser(user string) (string, string, error) {
splt := strings.Split(user, "@")
if len(splt) != 2 {
return "", "", fmt.Errorf("invalid username: %s", user)
}
if len(splt[0]) == 0 || len(splt[1]) == 0 {
return "", "", fmt.Errorf("invalid username: %s", user)
}
return splt[0], splt[1], nil
}
func buildUserQuery(arg string) (*models.User, error) {
var userQuery models.User
if uuidRegex.Match([]byte(arg)) {
id, err := uuid.Parse(arg)
if err != nil {
return nil, err
}
userQuery.ID = id
} else {
username, domain, err := splitUser(arg)
if err != nil {
return nil, err
}
userQuery = models.User{Domain: domain, Username: username}
}
return &userQuery, nil
}

45
pkg/config/config.go Normal file
View file

@ -0,0 +1,45 @@
package config
import (
"os"
providerConfigs "git.maurice.fr/thomas/mailout/pkg/providers/configs"
"gopkg.in/yaml.v3"
)
type Config struct {
Postgres struct {
Hostname string `yaml:"hostname"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
SSLMode string `yaml:"sslmode"`
} `yaml:"postgres"`
Providers struct {
OVH *providerConfigs.OVHConfig `yaml:"ovh"`
} `yaml:"providers"`
}
func LoadConfig(path string) (*Config, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
err = yaml.Unmarshal(b, &cfg)
if err != nil {
return nil, err
}
if cfg.Postgres.SSLMode == "" {
cfg.Postgres.SSLMode = "disable"
}
if cfg.Postgres.Port == 0 {
cfg.Postgres.Port = 5432
}
return &cfg, err
}

31
pkg/crypto/key.go Normal file
View file

@ -0,0 +1,31 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
)
func GenerateKeyPair(bytes int) (string, string, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, bytes)
if err != nil {
return "", "", err
}
err = privateKey.Validate()
if err != nil {
return "", "", err
}
privBlock := pem.Block{
Type: "PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
privateKeyPEM := pem.EncodeToMemory(&privBlock)
return string(privateKeyPEM), string(base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(&privateKey.PublicKey))), nil
}

74
pkg/database/db.go Normal file
View file

@ -0,0 +1,74 @@
package database
import (
"fmt"
"git.maurice.fr/thomas/mailout/pkg/config"
"git.maurice.fr/thomas/mailout/pkg/models"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
const (
createViewRawSQL = `
/*
This view basically returns to you the most recent key for
each known domain, so that when opendkim performs a where
filter on the result only one result will be returned.
Hence avoiding to make it confused and upset
*/
CREATE OR REPLACE VIEW signing_table AS
SELECT
k1.id,
k1.domain_name,
k1.created_at
FROM
dkimkeys AS k1
INNER JOIN
dkimkeys AS k2
ON
k1.created_at = (
SELECT
max(created_at)
FROM
dkimkeys
WHERE
domain_name = k1.domain_name
AND k1.active = true
)
WHERE k1.domain_name = k2.domain_name
AND k1.created_at = k2.created_at;
`
)
func NewDB(cfg *config.Config) (*gorm.DB, error) {
db, err := gorm.Open(
postgres.Open(
fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Postgres.Hostname,
cfg.Postgres.Port,
cfg.Postgres.User,
cfg.Postgres.Password,
cfg.Postgres.Database,
cfg.Postgres.SSLMode,
),
),
&gorm.Config{})
return db, err
}
func InitMigrate(db *gorm.DB) error {
err := db.AutoMigrate(&models.User{}, &models.DKIMKey{})
if err != nil {
return fmt.Errorf("could not auto migrate database: %w", err)
}
err = db.Exec(createViewRawSQL).Error
if err != nil {
return fmt.Errorf("could not create the view: %w", err)
}
return nil
}

23
pkg/models/dkimkey.go Normal file
View file

@ -0,0 +1,23 @@
package models
import (
"time"
"github.com/google/uuid"
)
type DKIMKey struct {
ID uuid.UUID `gorm:"primaryKey;column:id;type:uuid;default:gen_random_uuid()" yaml:"id"`
Selector string `gorm:"column:selector;type:varchar(128);not null" yaml:"selector"`
DomainName string `gorm:"column:domain_name;type:varchar(256);not null" yaml:"domain_name"`
PublicKey string `gorm:"column:public_key;type:text;not null" yaml:"public_key"`
PrivateKey string `gorm:"column:private_key;type:text;not null" yaml:"private_key"`
Record string `gorm:"column:record;type:text;not null" yaml:"record"`
Active bool `gorm:"column:active;type:boolean;not null;default:true" yaml:"active"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;autoCreateTime:nano;default:now()" yaml:"created_at"`
UpdatedAt time.Time `gorm:"column:udpated_at;type:timestamp;not null;autoUpdateTime:nano;default:now()" yaml:"updated_at"`
}
func (o *DKIMKey) TableName() string {
return "dkimkeys"
}

24
pkg/models/user.go Normal file
View file

@ -0,0 +1,24 @@
package models
import (
"time"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid()" yaml:"id"`
Username string `gorm:"primaryKey;column:username;type:varchar(256);not null" yaml:"username"`
Domain string `gorm:"primaryKey;column:domain;type:varchar(256);not null" yaml:"domain"`
Password string `gorm:"column:password;type:varchar(256);not null" yaml:"password"`
Home string `gorm:"column:home;type:varchar(256);not null;default:/var/lib/vmail/null" yaml:"home"`
UID int `gorm:"column:uid;type:integer;not null;default:1000" yaml:"uid"`
GID int `gorm:"column:gid;type:integer;not null;default:1000" yaml:"gid"`
Active bool `gorm:"column:active;type:boolean;not null;default:true" yaml:"active"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;autoCreateTime:nano;default:now()" yaml:"created_at"`
UpdatedAt time.Time `gorm:"column:udpated_at;type:timestamp;not null;autoUpdateTime:nano;default:now()" yaml:"updated_at"`
}
func (o *User) TableName() string {
return "users"
}

View file

@ -0,0 +1,8 @@
package configs
type OVHConfig struct {
Endpoint string `yaml:"endpoint"`
ApplicationKey string `yaml:"application_key"`
ApplicationSecret string `yaml:"application_secret"`
ConsumerKey string `yaml:"consumer_key"`
}

35
pkg/providers/ovh/ovh.go Normal file
View file

@ -0,0 +1,35 @@
package ovh
import (
"fmt"
"git.maurice.fr/thomas/mailout/pkg/config"
ovhgo "github.com/ovh/go-ovh/ovh"
)
type OVHProvider struct {
Client *ovhgo.Client
}
func NewOVHProvider(cfg *config.Config) (*OVHProvider, error) {
config := cfg.Providers.OVH
if config == nil {
return nil, fmt.Errorf("no ovh configuration specified")
}
c, err := ovhgo.NewClient(
config.Endpoint,
config.ApplicationKey,
config.ApplicationSecret,
config.ConsumerKey,
)
if err != nil {
return nil, err
}
return &OVHProvider{
Client: c,
}, nil
}

View file

@ -0,0 +1 @@
package provider

29
pkg/utils/domain.go Normal file
View file

@ -0,0 +1,29 @@
package utils
import (
"fmt"
"strings"
)
func GetZone(domain string) (string, error) {
splt := strings.Split(domain, ".")
if len(splt) == 1 {
return "", fmt.Errorf("invalid domain %s", domain)
}
if len(splt) == 2 {
return domain, nil
}
return splt[len(splt)-2] + "." + splt[len(splt)-1], nil
}
func GetSubdomain(domain string) (string, error) {
top, err := GetZone(domain)
if err != nil {
return "", err
}
sub := strings.TrimSuffix(domain, top)
return strings.TrimSuffix(sub, "."), nil
}