add basics

This commit is contained in:
Adora Laura Kalb 2024-07-24 17:36:01 +02:00
commit 710b99d02d
Signed by: adoralaura
SSH key fingerprint: SHA256:3XrkbR8ikAZJVtYfaUliX1MhmJYVAe/ocIb/MiDHBJ8
12 changed files with 299 additions and 0 deletions

25
.gitignore vendored Normal file
View file

@ -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
!*/

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright © 2024 Adora Laura Kalb <dev@lauka.net>
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.

12
examples/config.yaml Normal file
View file

@ -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

View file

@ -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

View file

@ -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

7
go.mod Normal file
View file

@ -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

5
go.sum Normal file
View file

@ -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=

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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
}

11
internal/misc/misc.go Normal file
View file

@ -0,0 +1,11 @@
package misc
func StringIsInStringSlice(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}