add basics
This commit is contained in:
commit
710b99d02d
12 changed files with 299 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal 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
9
LICENSE
Normal 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
12
examples/config.yaml
Normal 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
|
14
examples/mailcow-admin-aliases.service
Normal file
14
examples/mailcow-admin-aliases.service
Normal 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
|
11
examples/mailcow-admin-aliases.timer
Normal file
11
examples/mailcow-admin-aliases.timer
Normal 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
7
go.mod
Normal 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
5
go.sum
Normal 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=
|
55
internal/configuration/configuration.go
Normal file
55
internal/configuration/configuration.go
Normal 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
|
||||
|
||||
}
|
21
internal/logging/logger.go
Normal file
21
internal/logging/logger.go
Normal 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))
|
||||
}
|
75
internal/mailcow/aliases.go
Normal file
75
internal/mailcow/aliases.go
Normal 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
|
||||
}
|
54
internal/mailcow/domains.go
Normal file
54
internal/mailcow/domains.go
Normal 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
11
internal/misc/misc.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue