commit 710b99d02dde5d243e754c30c71583c942ca27cf Author: Adora Laura Kalb Date: Wed Jul 24 17:36:01 2024 +0200 add basics diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7752fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Allowlisting gitignore template for GO projects prevents us +# from adding various unwanted local files, such as generated +# files, developer configurations or IDE-specific files etc. +# +# Recommended: Go.AllowList.gitignore + +# Ignore everything +* + +# But not these files... +!/.gitignore + +!*.go +!go.sum +!go.mod + +!examples/* + +!*.md +!LICENSE + +# !Makefile + +# ...even if they are in subdirectories +!*/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..40ed8f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright © 2024 Adora Laura Kalb + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/examples/config.yaml b/examples/config.yaml new file mode 100644 index 0000000..e2aff6a --- /dev/null +++ b/examples/config.yaml @@ -0,0 +1,12 @@ +--- +api_endpoint: https://mailcow.example.com +api_key: aaaaa-bbbbb-ccccc-ddddd-eeeee +admin_email: admin@example.com + +mail_prefixes: + - abuse + - admin + - postmaster + - security + - webadmin + - webmaster diff --git a/examples/mailcow-admin-aliases.service b/examples/mailcow-admin-aliases.service new file mode 100644 index 0000000..346514b --- /dev/null +++ b/examples/mailcow-admin-aliases.service @@ -0,0 +1,14 @@ +[Unit] +Description=Mailcow Admin Alias Automation +Documentation=https://code.lila.network/adoralaura/mailcow-admin-aliases + +[Service] +# uncomment if you want to use a different user than root +# User=automation +# Group=automation +WorkingDirectory=/opt/mailcow-admin-aliases +ExecStart=/usr/local/bin/mailcow-admin-aliases + + +[Install] +WantedBy=multi-user.target diff --git a/examples/mailcow-admin-aliases.timer b/examples/mailcow-admin-aliases.timer new file mode 100644 index 0000000..40714ae --- /dev/null +++ b/examples/mailcow-admin-aliases.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Timer for mailcow-admin-aliases +Documentation=https://code.lila.network/adoralaura/mailcow-admin-aliases + +[Timer] +Persistent=true +OnCalendar=*-*-* 04:00:00 # every day between 2am and 6am +RandomizedDelaySec=2h + +[Install] +WantedBy=timers.target diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca4ca1f --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module code.lila.network/adoralaura/mailcow-admin-aliases + +go 1.22.2 + +require gopkg.in/yaml.v2 v2.4.0 + +require github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..88186b8 --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go new file mode 100644 index 0000000..01bdf3d --- /dev/null +++ b/internal/configuration/configuration.go @@ -0,0 +1,55 @@ +package configuration + +import ( + "fmt" + "log/slog" + "os" + + "gopkg.in/yaml.v2" +) + +const ( + AllDomainsApiEndpoint = "/api/v1/get/domain/all" + AllAliasesApiEndpoint = "/api/v1/get/alias/all" + AliasAddApiEndpoint = "/api/v1/add/alias" +) + +var ( + ConfigFile string + DryRun bool + Quiet bool +) + +type Config struct { + ApiEndpoint string `yaml:"api_endpoint"` + ApiKey string `yaml:"api_key"` + AdminEmail string `yaml:"admin_email"` + MailPrefixes []string `yaml:"mail_prefixes"` +} + +func (c Config) LoadFromDisk() error { + if ConfigFile == "" { + slog.Debug("no config file found, looking in working dir") + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + ConfigFile = wd + "/config.yaml" + } + slog.Debug("looking for config file", "path", ConfigFile) + + data, err := os.ReadFile(ConfigFile) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + err = yaml.Unmarshal(data, &c) + if err != nil { + return fmt.Errorf("failed to unmarshal config file: %w", err) + } + + slog.Debug("successfully read config file", "path", ConfigFile) + + return nil + +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..920ba2e --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,21 @@ +package logging + +import ( + "log/slog" + "os" +) + +func NewSlogLogger(quiet bool, verbose bool) { + slogLevel := slog.LevelInfo + if quiet { + slogLevel = slog.LevelError + } + + if verbose { + slogLevel = slog.LevelDebug + } + + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slogLevel}) + + slog.SetDefault(slog.New(handler)) +} diff --git a/internal/mailcow/aliases.go b/internal/mailcow/aliases.go new file mode 100644 index 0000000..1be7cc3 --- /dev/null +++ b/internal/mailcow/aliases.go @@ -0,0 +1,75 @@ +package mailcow + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "time" + + "code.lila.network/adoralaura/mailcow-admin-aliases/internal/configuration" +) + +type MailcowAlias struct { + ID int `json:"id"` + Domain string `json:"domain"` + PublicComment string `json:"public_comment"` + PrivateComment string `json:"private_comment"` + Goto string `json:"goto"` + Address string `json:"address"` + IsCatchAll int `json:"is_catch_all"` // skip if 1 + Active int `json:"active"` + ActiveInt int `json:"active_int"` + SogoVisible int `json:"sogo_visible"` + SogoVisibleInt int `json:"sogo_visible_int"` +} + +func NewAlias(alias string, destination string) MailcowAlias { + var a MailcowAlias + a.Active = 1 + a.PublicComment = "automatically generated by the mail server admin" + a.PrivateComment = "automatically generated by the mail server admin" + a.Address = alias + a.Goto = destination + + return a +} + +func LoadAliases(cfg configuration.Config) ([]MailcowAlias, error) { + var domains []MailcowAlias + + url := cfg.ApiEndpoint + configuration.AllAliasesApiEndpoint + method := "GET" + + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest(method, url, nil) + + slog.Debug("alias request", "method", method, "url", url) + + if err != nil { + return []MailcowAlias{}, fmt.Errorf("failed to create alias http request: %w", err) + } + req.Header.Add("accept", "application/json") + req.Header.Add("X-API-Key", cfg.ApiKey) + + res, err := client.Do(req) + if err != nil { + return []MailcowAlias{}, fmt.Errorf("failed to request aliases from server: %w", err) + } + defer res.Body.Close() + + slog.Debug("alias response received", "status", res.Status, "status-code", res.StatusCode) + + body, err := io.ReadAll(res.Body) + if err != nil { + return []MailcowAlias{}, fmt.Errorf("failed to read alias request body: %w", err) + } + + err = json.Unmarshal(body, &domains) + if err != nil { + return []MailcowAlias{}, fmt.Errorf("failed to unmarshal alias request body: %w", err) + } + + return domains, nil +} diff --git a/internal/mailcow/domains.go b/internal/mailcow/domains.go new file mode 100644 index 0000000..19f6747 --- /dev/null +++ b/internal/mailcow/domains.go @@ -0,0 +1,54 @@ +package mailcow + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "time" + + "code.lila.network/adoralaura/mailcow-admin-aliases/internal/configuration" +) + +type MailcowDomain struct { + DomainName string `json:"domain_name"` +} + +func LoadDomains(cfg configuration.Config) ([]MailcowDomain, error) { + var domains []MailcowDomain + + url := cfg.ApiEndpoint + configuration.AllDomainsApiEndpoint + method := "GET" + + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest(method, url, nil) + slog.Debug("domain request", "method", method, "url", url) + + if err != nil { + return []MailcowDomain{}, fmt.Errorf("failed to create http request: %w", err) + } + req.Header.Add("accept", "application/json") + req.Header.Add("X-API-Key", cfg.ApiKey) + + res, err := client.Do(req) + if err != nil { + return []MailcowDomain{}, fmt.Errorf("failed to request domains from server: %w", err) + } + defer res.Body.Close() + + slog.Debug("domain response received", "status", res.Status, "status-code", res.StatusCode) + + body, err := io.ReadAll(res.Body) + + if err != nil { + return []MailcowDomain{}, fmt.Errorf("failed to read domain request body: %w", err) + } + + err = json.Unmarshal(body, &domains) + if err != nil { + return []MailcowDomain{}, fmt.Errorf("failed to unmarshal domain request body: %w", err) + } + + return domains, nil +} diff --git a/internal/misc/misc.go b/internal/misc/misc.go new file mode 100644 index 0000000..55d1195 --- /dev/null +++ b/internal/misc/misc.go @@ -0,0 +1,11 @@ +package misc + +func StringIsInStringSlice(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + + return false +}