Compare commits
4 Commits
v0.9.0
...
9447523c6b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9447523c6b | ||
|
|
6845060d00 | ||
|
|
762cb11389 | ||
|
|
a7e6b5420e |
@@ -25,7 +25,7 @@ jobs:
|
|||||||
go-version: "1.22.x"
|
go-version: "1.22.x"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Build (linux/musl)
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
@@ -34,20 +34,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
BIN="kc-policy-${GOOS}-${GOARCH}"
|
BIN="mailcloak-${GOOS}-${GOARCH}"
|
||||||
go build \
|
go build \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-s -w -X main.version=${VERSION}" \
|
-ldflags="-s -w -X main.version=${VERSION}" \
|
||||||
-o "dist/${BIN}" ./cmd/kc-policy
|
-o "dist/${BIN}" ./cmd/mailcloak
|
||||||
tar -C dist -czf "dist/${BIN}.tar.gz" "${BIN}"
|
tar -C dist -czf "dist/${BIN}.tar.gz" "${BIN}"
|
||||||
|
cp mailcloakctl dist/
|
||||||
sha256sum "dist/${BIN}.tar.gz" >> dist/checksums.txt
|
sha256sum "dist/${BIN}.tar.gz" >> dist/checksums.txt
|
||||||
|
sha256sum "dist/mailcloakctl" >> dist/checksums.txt
|
||||||
|
|
||||||
- name: Upload assets to Gitea release
|
- name: Upload assets to Gitea release
|
||||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.tar.gz
|
dist/*.tar.gz
|
||||||
dist/*.sha256
|
dist/mailcloakctl
|
||||||
|
dist/checksums.txt
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ github.token }}
|
GITEA_TOKEN: ${{ github.token }}
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -1,10 +1,14 @@
|
|||||||
BINARY := kc-policy
|
BINARY := mailcloak
|
||||||
BIN_DIR := bin
|
BIN_DIR := bin
|
||||||
|
|
||||||
.PHONY: build run test tidy clean install
|
.PHONY: build run test tidy clean install
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -o $(BIN_DIR)/$(BINARY) ./cmd/$(BINARY)
|
export CGO_ENABLED=0
|
||||||
|
go build \
|
||||||
|
-trimpath \
|
||||||
|
-ldflags="-s -w" \
|
||||||
|
-o "$(BIN_DIR)/$(BINARY)" ./cmd/$(BINARY)
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go run ./cmd/$(BINARY)
|
go run ./cmd/$(BINARY)
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -1,4 +1,4 @@
|
|||||||
# kc-policy
|
# mailcloak
|
||||||
|
|
||||||
Postfix policy + socketmap daemon that validates recipients/senders against Keycloak and serves a local aliases SQLite database.
|
Postfix policy + socketmap daemon that validates recipients/senders against Keycloak and serves a local aliases SQLite database.
|
||||||
|
|
||||||
@@ -9,13 +9,13 @@ Postfix policy + socketmap daemon that validates recipients/senders against Keyc
|
|||||||
- **Socketmap service**: exposes an `alias` map to Postfix, rewriting alias -> `username@domain`.
|
- **Socketmap service**: exposes an `alias` map to Postfix, rewriting alias -> `username@domain`.
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
- `cmd/kc-policy/` – main package entrypoint
|
- `cmd/mailcloak/` – main package entrypoint
|
||||||
- `internal/kcpolicy/` – daemon sources
|
- `internal/mailcloak/` – daemon sources
|
||||||
- `go.mod` / `go.sum` – Go module files
|
- `go.mod` / `go.sum` – Go module files
|
||||||
- `configs/config.yaml.sample` – sample config to copy to `/etc/kc-policy/config.yaml`
|
- `configs/config.yaml.sample` – sample config to copy to `/etc/mailcloak/config.yaml`
|
||||||
- `configs/openrc-kc-policy` – OpenRC service file
|
- `configs/openrc-mailcloak` – OpenRC service file
|
||||||
- `db-init.sql` – SQLite schema (also auto-created by the app)
|
- `db-init.sql` – SQLite schema (also auto-created by the app)
|
||||||
- `aliasctl.py` – CLI helper to manage aliases
|
- `mailcloakctl` – CLI helper to manage aliases
|
||||||
|
|
||||||
## Build the binary
|
## Build the binary
|
||||||
From the repository root:
|
From the repository root:
|
||||||
@@ -40,8 +40,8 @@ make run
|
|||||||
Copy the sample config and edit it:
|
Copy the sample config and edit it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
install -d -m 0750 -o root -g postfix /etc/kc-policy
|
install -d -m 0750 -o root -g postfix /etc/mailcloak
|
||||||
cp configs/config.yaml.sample /etc/kc-policy/config.yaml
|
cp configs/config.yaml.sample /etc/mailcloak/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Key settings:
|
Key settings:
|
||||||
@@ -50,12 +50,12 @@ Key settings:
|
|||||||
- `sqlite.path` is the aliases database path.
|
- `sqlite.path` is the aliases database path.
|
||||||
- `sockets.*` must be under the Postfix chroot (usually `/var/spool/postfix`).
|
- `sockets.*` must be under the Postfix chroot (usually `/var/spool/postfix`).
|
||||||
|
|
||||||
## Alias database
|
## Mailcloak database
|
||||||
You can manage aliases using the helper script:
|
You can manage aliases using the helper script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./aliasctl.py --db /var/lib/kc-policy/aliases.db add alias@example.com username
|
./mailcloakctl aliases add alias@example.com username
|
||||||
./aliasctl.py --db /var/lib/kc-policy/aliases.db list
|
./mailcloakctl aliases list
|
||||||
```
|
```
|
||||||
|
|
||||||
The script creates the schema automatically if missing.
|
The script creates the schema automatically if missing.
|
||||||
@@ -63,21 +63,21 @@ The script creates the schema automatically if missing.
|
|||||||
## Postfix integration (example)
|
## Postfix integration (example)
|
||||||
Policy service (smtpd_recipient_restrictions):
|
Policy service (smtpd_recipient_restrictions):
|
||||||
```
|
```
|
||||||
check_policy_service unix:private/kc-policy
|
check_policy_service unix:private/mailcloak
|
||||||
```
|
```
|
||||||
|
|
||||||
Socketmap (virtual_alias_maps):
|
Socketmap (virtual_alias_maps):
|
||||||
```
|
```
|
||||||
socketmap:unix:private/kc-socketmap:alias
|
socketmap:unix:private/mailcloak-socketmap:alias
|
||||||
```
|
```
|
||||||
|
|
||||||
## OpenRC
|
## OpenRC
|
||||||
Use the provided service file:
|
Use the provided service file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp configs/openrc-kc-policy /etc/init.d/kc-policy
|
cp configs/openrc-mailcloak /etc/init.d/mailcloak
|
||||||
rc-update add kc-policy default
|
rc-update add mailcloak default
|
||||||
rc-service kc-policy start
|
rc-service mailcloak start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"kc-policy/internal/kcpolicy"
|
"mailcloak/internal/mailcloak"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "dev"
|
var version = "dev"
|
||||||
@@ -20,43 +20,43 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgPath := "/etc/kc-policy/config.yaml"
|
cfgPath := "/etc/mailcloak/config.yaml"
|
||||||
if len(os.Args) >= 2 {
|
if len(os.Args) >= 2 {
|
||||||
cfgPath = os.Args[1]
|
cfgPath = os.Args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := kcpolicy.LoadConfig(cfgPath)
|
cfg, err := mailcloak.LoadConfig(cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("config: %v", err)
|
log.Fatalf("config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := kcpolicy.OpenAliasDB(cfg.SQLite.Path)
|
db, err := mailcloak.OpenAliasDB(cfg.SQLite.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("sqlite: %v", err)
|
log.Fatalf("sqlite: %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
kc := kcpolicy.NewKeycloak(cfg)
|
kc := mailcloak.NewKeycloak(cfg)
|
||||||
cache := kcpolicy.NewCache(time.Duration(cfg.Policy.CacheTTLSeconds) * time.Second)
|
cache := mailcloak.NewCache(time.Duration(cfg.Policy.CacheTTLSeconds) * time.Second)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Start socketmap server
|
// Start socketmap server
|
||||||
go func() {
|
go func() {
|
||||||
if err := kcpolicy.RunSocketmap(ctx, cfg, db); err != nil {
|
if err := mailcloak.RunSocketmap(ctx, cfg, db); err != nil {
|
||||||
log.Fatalf("socketmap: %v", err)
|
log.Fatalf("socketmap: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Start policy server
|
// Start policy server
|
||||||
go func() {
|
go func() {
|
||||||
if err := kcpolicy.RunPolicy(ctx, cfg, db, kc, cache); err != nil {
|
if err := mailcloak.RunPolicy(ctx, cfg, db, kc, cache); err != nil {
|
||||||
log.Fatalf("policy: %v", err)
|
log.Fatalf("policy: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("kc-policy started")
|
log.Printf("mailcloak started")
|
||||||
|
|
||||||
// Handle signals
|
// Handle signals
|
||||||
ch := make(chan os.Signal, 2)
|
ch := make(chan os.Signal, 2)
|
||||||
@@ -6,7 +6,7 @@ keycloak:
|
|||||||
# admin API is derived: {base_url}/admin/realms/{realm}
|
# admin API is derived: {base_url}/admin/realms/{realm}
|
||||||
|
|
||||||
sqlite:
|
sqlite:
|
||||||
path: "/var/lib/kc-policy/aliases.db"
|
path: "/var/lib/mailcloak/state.db"
|
||||||
|
|
||||||
policy:
|
policy:
|
||||||
domain: "<EMail domain-name>"
|
domain: "<EMail domain-name>"
|
||||||
@@ -19,8 +19,8 @@ policy:
|
|||||||
|
|
||||||
sockets:
|
sockets:
|
||||||
# These paths must be inside postfix chroot (/var/spool/postfix)
|
# These paths must be inside postfix chroot (/var/spool/postfix)
|
||||||
policy_socket: "/var/spool/postfix/private/kc-policy"
|
policy_socket: "/var/spool/postfix/private/mailcloak-policy"
|
||||||
socketmap_socket: "/var/spool/postfix/private/kc-socketmap"
|
socketmap_socket: "/var/spool/postfix/private/mailcloak-socketmap"
|
||||||
socket_owner_user: "postfix"
|
socket_owner_user: "postfix"
|
||||||
socket_owner_group: "postfix"
|
socket_owner_group: "postfix"
|
||||||
socket_mode: "0660"
|
socket_mode: "0660"
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/sbin/openrc-run
|
|
||||||
|
|
||||||
name="kc-policy"
|
|
||||||
command="/usr/local/sbin/kc-policy"
|
|
||||||
command_args="/etc/kc-policy/config.yaml"
|
|
||||||
command_background="yes"
|
|
||||||
pidfile="/run/kc-policy.pid"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need net
|
|
||||||
after postfix
|
|
||||||
}
|
|
||||||
|
|
||||||
start_pre() {
|
|
||||||
checkpath -d -m 0750 -o root:postfix /etc/kc-policy
|
|
||||||
checkpath -d -m 0750 -o root:postfix /var/lib/kc-policy
|
|
||||||
checkpath -d -m 0755 -o root:root /usr/local/sbin
|
|
||||||
# sockets dir already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
stop_post() {
|
|
||||||
rm -f /var/spool/postfix/private/kc-policy /var/spool/postfix/private/kc-socketmap
|
|
||||||
}
|
|
||||||
23
configs/openrc-mailcloak
Normal file
23
configs/openrc-mailcloak
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/sbin/openrc-run
|
||||||
|
|
||||||
|
name="mailcloak"
|
||||||
|
command="/usr/local/sbin/mailcloak"
|
||||||
|
command_args="/etc/mailcloak/config.yaml"
|
||||||
|
command_background="yes"
|
||||||
|
pidfile="/run/mailcloak.pid"
|
||||||
|
|
||||||
|
depend() {
|
||||||
|
need net
|
||||||
|
after postfix
|
||||||
|
}
|
||||||
|
|
||||||
|
start_pre() {
|
||||||
|
checkpath -d -m 0750 -o root:postfix /etc/mailcloak
|
||||||
|
checkpath -d -m 0750 -o root:postfix /var/lib/mailcloak
|
||||||
|
checkpath -d -m 0755 -o root:root /usr/local/sbin
|
||||||
|
# sockets dir already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_post() {
|
||||||
|
rm -f /var/spool/postfix/private/mailcloak-policy /var/spool/postfix/private/mailcloak-socketmap
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ virtual_mailbox_domains = static:<EMail domain-name>
|
|||||||
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||||
|
|
||||||
# Dynamic aliases via socketmap
|
# Dynamic aliases via socketmap
|
||||||
virtual_alias_maps = socketmap:unix:private/kc-socketmap:alias
|
virtual_alias_maps = socketmap:unix:private/mailcloak-socketmap:alias
|
||||||
|
|
||||||
# Policy (RCPT existence + sender policy on 587 via master.cf)
|
# Policy (RCPT existence + sender policy on 587 via master.cf)
|
||||||
smtpd_recipient_restrictions =
|
smtpd_recipient_restrictions =
|
||||||
@@ -17,5 +17,5 @@ smtpd_recipient_restrictions =
|
|||||||
reject_unknown_recipient_domain,
|
reject_unknown_recipient_domain,
|
||||||
permit_sasl_authenticated,
|
permit_sasl_authenticated,
|
||||||
reject_unauth_destination,
|
reject_unauth_destination,
|
||||||
check_policy_service unix:private/kc-policy,
|
check_policy_service unix:private/mailcloak-policy,
|
||||||
permit
|
permit
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Configuration to add to /etc/postfix/master.cf
|
# Configuration to add to /etc/postfix/master.cf
|
||||||
#
|
#
|
||||||
|
|
||||||
-o smtpd_sender_restrictions=check_policy_service unix:private/kc-policy
|
-o smtpd_sender_restrictions=check_policy_service unix:private/mailcloak-policy
|
||||||
|
|
||||||
# You can remove `reject_senders_login_mismaych` + `sender_login_maps`
|
# You can remove `reject_senders_login_mismatch` + `sender_login_maps`
|
||||||
# as this kc-policy will handle it.
|
# as mailcloak will handle it.
|
||||||
15
db-init.sql
15
db-init.sql
@@ -6,3 +6,18 @@ CREATE TABLE IF NOT EXISTS aliases (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_aliases_username ON aliases(username);
|
CREATE INDEX IF NOT EXISTS idx_aliases_username ON aliases(username);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS apps (
|
||||||
|
app_id TEXT PRIMARY KEY,
|
||||||
|
secret_hash TEXT NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app_from (
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
from_addr TEXT NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (app_id, from_addr),
|
||||||
|
FOREIGN KEY (app_id) REFERENCES apps(app_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package kcpolicy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunSocketmap(ctx context.Context, cfg *Config, db *AliasDB) error {
|
|
||||||
sock := cfg.Sockets.SocketmapSocket
|
|
||||||
_ = os.Remove(sock)
|
|
||||||
|
|
||||||
l, err := net.Listen("unix", sock)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer l.Close()
|
|
||||||
|
|
||||||
if err := ChownChmodSocket(sock, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go handleSocketmapConn(conn, cfg, db)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Socketmap protocol: "mapname key\n" -> "OK value\n" or "NOTFOUND\n" or "TEMP\n"
|
|
||||||
func handleSocketmapConn(conn net.Conn, cfg *Config, db *AliasDB) {
|
|
||||||
defer conn.Close()
|
|
||||||
r := bufio.NewReader(conn)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := r.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(line, " ", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
fmt.Fprint(conn, "TEMP\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mapName := parts[0]
|
|
||||||
key := strings.ToLower(strings.TrimSpace(parts[1]))
|
|
||||||
|
|
||||||
if mapName != "alias" {
|
|
||||||
fmt.Fprint(conn, "NOTFOUND\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only handle our domain
|
|
||||||
if !strings.HasSuffix(key, "@"+strings.ToLower(cfg.Policy.Domain)) {
|
|
||||||
fmt.Fprint(conn, "NOTFOUND\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
username, ok, err := db.AliasOwner(key)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprint(conn, "TEMP\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
fmt.Fprint(conn, "NOTFOUND\n")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// rewrite alias -> primary rcpt (username@domain)
|
|
||||||
fmt.Fprintf(conn, "OK %s@%s\n", username, strings.ToLower(cfg.Policy.Domain))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package kcpolicy
|
package mailcloak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package kcpolicy
|
package mailcloak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package kcpolicy
|
package mailcloak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package kcpolicy
|
package mailcloak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
142
internal/mailcloak/socketmap.go
Normal file
142
internal/mailcloak/socketmap.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package mailcloak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunSocketmap(ctx context.Context, cfg *Config, db *AliasDB) error {
|
||||||
|
sock := cfg.Sockets.SocketmapSocket
|
||||||
|
_ = os.Remove(sock)
|
||||||
|
|
||||||
|
l, err := net.Listen("unix", sock)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
if err := ChownChmodSocket(sock, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go handleSocketmapConn(conn, cfg, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postfix socketmap framing: "<len>:<payload>,"
|
||||||
|
func handleSocketmapConn(conn net.Conn, cfg *Config, db *AliasDB) {
|
||||||
|
defer conn.Close()
|
||||||
|
r := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
for {
|
||||||
|
payload, err := readSocketmapFrame(r)
|
||||||
|
if err != nil {
|
||||||
|
// normal close
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = strings.TrimSpace(payload)
|
||||||
|
if payload == "" {
|
||||||
|
_ = writeSocketmapFrame(conn, "NOTFOUND")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(payload, " ", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
_ = writeSocketmapFrame(conn, "TEMP")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mapName := parts[0]
|
||||||
|
key := strings.ToLower(strings.TrimSpace(parts[1]))
|
||||||
|
|
||||||
|
if mapName != "alias" {
|
||||||
|
_ = writeSocketmapFrame(conn, "NOTFOUND")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only handle our domain
|
||||||
|
domain := strings.ToLower(cfg.Policy.Domain)
|
||||||
|
if !strings.HasSuffix(key, "@"+domain) {
|
||||||
|
_ = writeSocketmapFrame(conn, "NOTFOUND")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
username, ok, err := db.AliasOwner(key)
|
||||||
|
if err != nil {
|
||||||
|
_ = writeSocketmapFrame(conn, "TEMP")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
_ = writeSocketmapFrame(conn, "NOTFOUND")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite alias -> username@domain
|
||||||
|
_ = writeSocketmapFrame(conn, fmt.Sprintf("OK %s@%s", username, domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSocketmapFrame(r *bufio.Reader) (string, error) {
|
||||||
|
// read decimal length until ':'
|
||||||
|
var lenBuf strings.Builder
|
||||||
|
for {
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if b == ':' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return "", io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
lenBuf.WriteByte(b)
|
||||||
|
if lenBuf.Len() > 10 {
|
||||||
|
return "", io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := strconv.Atoi(lenBuf.String())
|
||||||
|
if err != nil || n < 0 || n > 1024*1024 {
|
||||||
|
return "", io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, n)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect trailing comma
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if b != ',' {
|
||||||
|
return "", io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSocketmapFrame(w io.Writer, payload string) error {
|
||||||
|
// "<len>:<payload>,"
|
||||||
|
_, err := fmt.Fprintf(w, "%d:%s,", len(payload), payload)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package kcpolicy
|
package mailcloak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
@@ -4,7 +4,7 @@ import sqlite3
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DEFAULT_DB = "/var/lib/kc-policy/aliases.db"
|
DEFAULT_DB = "/var/lib/mailcloak/state.db"
|
||||||
|
|
||||||
SCHEMA = """
|
SCHEMA = """
|
||||||
CREATE TABLE IF NOT EXISTS aliases (
|
CREATE TABLE IF NOT EXISTS aliases (
|
||||||
Reference in New Issue
Block a user