From b9fff8c053d04577d045c95a4c396db739b9d1aa Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 13 Mar 2026 11:55:39 +0100 Subject: [PATCH 01/22] feat(api): update domain.go --- api/internal/repository/domain.go | 6 ++++ api/internal/service/domain.go | 11 ++++++++ api/internal/transport/api/domain.go | 41 ++++++++++++++++++++++++++++ api/internal/transport/api/routes.go | 1 + 4 files changed, 59 insertions(+) diff --git a/api/internal/repository/domain.go b/api/internal/repository/domain.go index 99f6659..01b2602 100644 --- a/api/internal/repository/domain.go +++ b/api/internal/repository/domain.go @@ -30,6 +30,12 @@ func (d *Database) GetVerifiedDomain(ctx context.Context, domainID string, userI return domain, err } +func (d *Database) GetVerifiedDomainByName(ctx context.Context, domainName string) (model.Domain, error) { + var domain model.Domain + err := d.Client.Where("name = ? AND owner_verified_at IS NOT NULL AND mx_verified_at IS NOT NULL AND send_verified_at IS NOT NULL", domainName).First(&domain).Error + return domain, err +} + func (d *Database) GetDomainsCount(ctx context.Context, userID string) (int64, error) { var count int64 err := d.Client.Model(&model.Domain{}).Where("user_id = ?", userID).Count(&count).Error diff --git a/api/internal/service/domain.go b/api/internal/service/domain.go index 2241d6c..bc64535 100644 --- a/api/internal/service/domain.go +++ b/api/internal/service/domain.go @@ -34,6 +34,7 @@ type DomainStore interface { GetVerifiedDomains(context.Context, string) ([]model.Domain, error) GetDomain(context.Context, string, string) (model.Domain, error) GetVerifiedDomain(context.Context, string, string) (model.Domain, error) + GetVerifiedDomainByName(context.Context, string) (model.Domain, error) GetDomainsCount(context.Context, string) (int64, error) PostDomain(context.Context, model.Domain) (model.Domain, error) UpdateDomain(context.Context, model.Domain) error @@ -81,6 +82,16 @@ func (s *Service) GetVerifiedDomain(ctx context.Context, domainID string, userID return domain, nil } +func (s *Service) GetVerifiedDomainByName(ctx context.Context, domainName string) (model.Domain, error) { + domain, err := s.Store.GetVerifiedDomainByName(ctx, domainName) + if err != nil { + log.Printf("error getting verified domain by name: %s", err.Error()) + return model.Domain{}, ErrGetDomain + } + + return domain, nil +} + func (s *Service) GetDomainsCount(ctx context.Context, userId string) (int64, error) { count, err := s.Store.GetDomainsCount(ctx, userId) if err != nil { diff --git a/api/internal/transport/api/domain.go b/api/internal/transport/api/domain.go index 9cd6173..27387d0 100644 --- a/api/internal/transport/api/domain.go +++ b/api/internal/transport/api/domain.go @@ -26,6 +26,7 @@ type DomainService interface { GetVerifiedDomains(context.Context, string) ([]model.Domain, error) GetDomain(context.Context, string, string) (model.Domain, error) GetVerifiedDomain(context.Context, string, string) (model.Domain, error) + GetVerifiedDomainByName(context.Context, string) (model.Domain, error) GetDNSConfig(context.Context, string) (model.DNSConfig, error) PostDomain(context.Context, model.Domain) (model.Domain, error) UpdateDomain(context.Context, model.Domain) error @@ -231,3 +232,43 @@ func (h *Handler) VerifyDomainDNSRecords(c *fiber.Ctx) error { "message": DNSRecordVerificationSuccess, }) } + +// @Summary Check custom domain +// @Description Check if a custom domain exists for the authenticated user +// @Tags domain +// @Accept json +// @Produce json +// @Param domain body DomainReq true "Custom Domain Request" +// @Success 200 {string} string "OK" +// @Failure 400 {object} ErrorRes +// @Router /email/domain/check [post] +func (h *Handler) CheckDomain(c *fiber.Ctx) error { + // Parse the request + req := DomainReq{} + err := c.BodyParser(&req) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": ErrInvalidRequest, + }) + } + + // Validate the request + err = h.Validator.Struct(req) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": ErrInvalidRequest, + }) + } + + // Check if domain exists + domain, err := h.Service.GetVerifiedDomainByName(c.Context(), req.Name) + if err != nil { + return c.Status(400).SendString("Not Found") + } + + if domain.Name != req.Name { + return c.Status(400).SendString("Not Found") + } + + return c.SendString("OK") +} diff --git a/api/internal/transport/api/routes.go b/api/internal/transport/api/routes.go index f239261..66051e7 100644 --- a/api/internal/transport/api/routes.go +++ b/api/internal/transport/api/routes.go @@ -17,6 +17,7 @@ func (h *Handler) SetupRoutes(cfg config.APIConfig) { email := h.Server.Group("/v1/email") email.Use(auth.NewPSK(cfg)) email.Post("", h.HandleEmail) + email.Post("/domain/check", h.CheckDomain) h.Server.Use(auth.NewAPICORS(cfg)) h.Server.Use(helmet.New()) From b11ac00112050806ff33b24e1bfcacd508e02b16 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Mon, 16 Mar 2026 13:54:22 +0100 Subject: [PATCH 02/22] feat(mailserver): create daemon/main.go --- mailserver/.env.sample | 1 + mailserver/daemon/go.mod | 3 + mailserver/daemon/main.go | 223 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 mailserver/daemon/go.mod create mode 100644 mailserver/daemon/main.go diff --git a/mailserver/.env.sample b/mailserver/.env.sample index 7381f37..a26a706 100644 --- a/mailserver/.env.sample +++ b/mailserver/.env.sample @@ -3,6 +3,7 @@ DMS_IP= NET_SUBNET= NET_GATEWAY= DOMAIN=example.com +DOMAINS=example.com,example.net SMTP_USER= SMTP_PASS= SSL_TYPE= diff --git a/mailserver/daemon/go.mod b/mailserver/daemon/go.mod new file mode 100644 index 0000000..292fc25 --- /dev/null +++ b/mailserver/daemon/go.mod @@ -0,0 +1,3 @@ +module daemon + +go 1.24 \ No newline at end of file diff --git a/mailserver/daemon/main.go b/mailserver/daemon/main.go new file mode 100644 index 0000000..8b53ef3 --- /dev/null +++ b/mailserver/daemon/main.go @@ -0,0 +1,223 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "strings" + "sync" + "time" +) + +type cacheEntry struct { + valid bool + exp time.Time +} + +var cache = map[string]cacheEntry{} +var mu sync.RWMutex + +var positiveTTL = 10 * time.Minute +var negativeTTL = 2 * time.Minute + +var localDomains = map[string]bool{} + +var apiURL string +var psk string + +type domainCheckRequest struct { + Name string `json:"name"` +} + +func main() { + loadConfig() + loadLocalDomains() + + ln, err := net.Listen("tcp", ":10025") + + if err != nil { + panic(err) + } + + fmt.Println("Domain Policy Service (daemon) listening on 10025") + + for { + conn, err := ln.Accept() + if err != nil { + continue + } + + go handle(conn) + } +} + +func loadConfig() { + apiURL = os.Getenv("API_URL") + psk = os.Getenv("PSK") + + if ttl := os.Getenv("CACHE_POSITIVE_TTL"); ttl != "" { + if d, err := time.ParseDuration(ttl); err == nil { + positiveTTL = d + } + } + + if ttl := os.Getenv("CACHE_NEGATIVE_TTL"); ttl != "" { + if d, err := time.ParseDuration(ttl); err == nil { + negativeTTL = d + } + } +} + +func loadLocalDomains() { + domains := os.Getenv("LOCAL_DOMAINS") + + if domains == "" { + return + } + + for _, d := range strings.Split(domains, ",") { + domain := strings.TrimSpace(d) + if domain != "" { + localDomains[domain] = true + } + } +} + +func handle(conn net.Conn) { + defer conn.Close() + conn.SetDeadline(time.Now().Add(10 * time.Second)) + scanner := bufio.NewScanner(conn) + var recipient string + + for scanner.Scan() { + line := scanner.Text() + + if line == "" { + break + } + + if strings.HasPrefix(line, "recipient=") { + recipient = strings.TrimPrefix(line, "recipient=") + } + } + + if recipient == "" { + fmt.Fprintf(conn, "action=DUNNO\n\n") + return + } + + parts := strings.Split(recipient, "@") + + if len(parts) != 2 { + fmt.Fprintf(conn, "action=REJECT invalid recipient\n\n") + return + } + + domain := strings.ToLower(parts[1]) + + // Skip check for local domains + if localDomains[domain] { + fmt.Fprintf(conn, "action=OK\n\n") + return + } + + // Check cache + if valid, found := checkCache(domain); found { + if valid { + fmt.Fprintf(conn, "action=OK\n\n") + } else { + fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") + } + return + } + + // API check + valid := checkDomain(domain) + storeCache(domain, valid) + + if valid { + fmt.Fprintf(conn, "action=OK\n\n") + } else { + fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") + } +} + +func checkCache(domain string) (bool, bool) { + mu.RLock() + entry, ok := cache[domain] + mu.RUnlock() + + if !ok { + return false, false + } + + if time.Now().After(entry.exp) { + mu.Lock() + delete(cache, domain) + mu.Unlock() + return false, false + } + + return entry.valid, true +} + +func storeCache(domain string, valid bool) { + var ttl time.Duration + + if valid { + ttl = positiveTTL + } else { + ttl = negativeTTL + } + + mu.Lock() + + cache[domain] = cacheEntry{ + valid: valid, + exp: time.Now().Add(ttl), + } + + mu.Unlock() +} + +func checkDomain(domain string) bool { + client := http.Client{ + Timeout: 5 * time.Second, + } + + payload := domainCheckRequest{ + Name: domain, + } + + jsonData, err := json.Marshal(payload) + + if err != nil { + return false + } + + req, err := http.NewRequest( + "POST", + apiURL+"/v1/email/domain/check", + bytes.NewBuffer(jsonData), + ) + + if err != nil { + return false + } + + req.Header.Set("Authorization", "Bearer "+psk) + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + + if err != nil { + return false + } + + defer resp.Body.Close() + + return resp.StatusCode == 200 +} From bb701345d03a75b4164002a91543a213250cae4f Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Mon, 16 Mar 2026 14:06:05 +0100 Subject: [PATCH 03/22] feat(mailserver): update compose.yml --- mailserver/compose.yml | 12 ++++++++++++ mailserver/daemon/Dockerfile | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 mailserver/daemon/Dockerfile diff --git a/mailserver/compose.yml b/mailserver/compose.yml index 0f1dc78..5c452ad 100644 --- a/mailserver/compose.yml +++ b/mailserver/compose.yml @@ -52,6 +52,18 @@ services: retries: 0 networks: email_net: + + daemon: + build: ./daemon + environment: + - API_URL=http://email-api:3000 + - PSK=${PSK} + - LOCAL_DOMAINS=${DOMAINS} + - CACHE_POSITIVE_TTL=10m + - CACHE_NEGATIVE_TTL=2m + restart: always + networks: + email_net: networks: email_net: diff --git a/mailserver/daemon/Dockerfile b/mailserver/daemon/Dockerfile new file mode 100644 index 0000000..e41d348 --- /dev/null +++ b/mailserver/daemon/Dockerfile @@ -0,0 +1,21 @@ +# stage 1: building application binary file +FROM golang:1.24 AS builder + +RUN mkdir /daemon +ADD . /daemon +WORKDIR /daemon + +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 + +RUN go build -o daemon main.go + +# stage 2: copy only the application binary file and necessary files to the alpine container +FROM alpine:latest AS production + +COPY --from=builder /daemon . + +# run the service on container startup +EXPOSE 10025 +CMD ["./daemon"] From 6d7d0aa8051850e6ab2619bcff8265e47932bbaf Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Mon, 16 Mar 2026 20:33:37 +0100 Subject: [PATCH 04/22] feat(mailserver): update compose.yml --- mailserver/compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mailserver/compose.yml b/mailserver/compose.yml index 5c452ad..d58c6b5 100644 --- a/mailserver/compose.yml +++ b/mailserver/compose.yml @@ -55,6 +55,7 @@ services: daemon: build: ./daemon + container_name: daemon environment: - API_URL=http://email-api:3000 - PSK=${PSK} From fbd93548a1b3cf0d56c8cfa8b22f54afc94e5c44 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Mon, 16 Mar 2026 20:45:50 +0100 Subject: [PATCH 05/22] ci: update ci_staging.yml --- .github/workflows/ci_staging.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci_staging.yml b/.github/workflows/ci_staging.yml index 6e52658..8a9afe5 100644 --- a/.github/workflows/ci_staging.yml +++ b/.github/workflows/ci_staging.yml @@ -11,6 +11,7 @@ permissions: env: API_IMAGE: email/api APP_IMAGE: email/app + DAEMON_IMAGE: mailserver/daemon TAG: staging REGISTRY: ${{ secrets.REGISTRY_NAME }} @@ -38,6 +39,9 @@ jobs: - name: Build app image run: docker build -t $REGISTRY/$APP_IMAGE:$TAG app/. + + - name: Build daemon image + run: docker build -t $REGISTRY/$DAEMON_IMAGE:$TAG mailserver/daemon/. - name: Log in to registry run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_NAME }} -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin @@ -47,3 +51,6 @@ jobs: - name: Push app:${{ env.TAG }} run: docker push $REGISTRY/$APP_IMAGE:$TAG + + - name: Push daemon:${{ env.TAG }} + run: docker push $REGISTRY/$DAEMON_IMAGE:$TAG \ No newline at end of file From 06d384409398eb1dae7b51688a217940e4ba9501 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Mon, 16 Mar 2026 22:31:19 +0100 Subject: [PATCH 06/22] feat(mailserver): update .env.sample --- mailserver/.env.sample | 5 +++++ mailserver/compose.yml | 2 +- mailserver/config/user-patches.sh.sample | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mailserver/.env.sample b/mailserver/.env.sample index a26a706..6bc44cf 100644 --- a/mailserver/.env.sample +++ b/mailserver/.env.sample @@ -1,3 +1,7 @@ +IMAGE_REGISTRY= +IMAGE_TAG=latest +DAEMON_IMAGE=/mailserver/daemon + DMS_HOSTNAME=mail.example.com DMS_IP= NET_SUBNET= @@ -11,4 +15,5 @@ ENABLE_RSPAMD=1 ENABLE_FAIL2BAN=1 ENABLE_CLAMAV=0 LOG_LEVEL=info +API_URL=http://email-api:3000 PSK="" diff --git a/mailserver/compose.yml b/mailserver/compose.yml index d58c6b5..1d9a68a 100644 --- a/mailserver/compose.yml +++ b/mailserver/compose.yml @@ -57,7 +57,7 @@ services: build: ./daemon container_name: daemon environment: - - API_URL=http://email-api:3000 + - API_URL=${API_URL} - PSK=${PSK} - LOCAL_DOMAINS=${DOMAINS} - CACHE_POSITIVE_TTL=10m diff --git a/mailserver/config/user-patches.sh.sample b/mailserver/config/user-patches.sh.sample index fe99b60..a5fdd8e 100644 --- a/mailserver/config/user-patches.sh.sample +++ b/mailserver/config/user-patches.sh.sample @@ -11,7 +11,7 @@ chmod 640 /etc/sasldb2 # curl-email.sh script echo "#!/bin/sh -curl --silent --fail --data-binary @- -H \"Authorization: Bearer $PSK\" -X POST http://email-api:3000/v1/email +curl --silent --fail --data-binary @- -H \"Authorization: Bearer $PSK\" -X POST $API_URL/v1/email rc=\$? From df528dbf063af66ae39049389b98367642a11c4b Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Tue, 17 Mar 2026 10:41:50 +0100 Subject: [PATCH 07/22] feat(mailserver): update compose.deploy.yml --- mailserver/compose.deploy.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mailserver/compose.deploy.yml b/mailserver/compose.deploy.yml index 96c2c6c..96b28ef 100644 --- a/mailserver/compose.deploy.yml +++ b/mailserver/compose.deploy.yml @@ -54,6 +54,19 @@ services: net: ipv4_address: ${DMS_IP} + daemon: + image: ${IMAGE_REGISTRY}${DAEMON_IMAGE}:${IMAGE_TAG} + container_name: daemon + environment: + - API_URL=${API_URL} + - PSK=${PSK} + - LOCAL_DOMAINS=${DOMAINS} + - CACHE_POSITIVE_TTL=10m + - CACHE_NEGATIVE_TTL=2m + restart: always + networks: + net: + networks: net: driver: bridge From 6e6087f9a87a5d0c2abd644622fc30f2ce8a93a0 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 11:58:29 +0100 Subject: [PATCH 08/22] feat(mailserver): create postfix-transport.cf.sample --- mailserver/config/postfix-main.cf.sample | 8 ++++++++ mailserver/config/postfix-transport.cf.sample | 1 + mailserver/config/user-patches.sh.sample | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 mailserver/config/postfix-transport.cf.sample diff --git a/mailserver/config/postfix-main.cf.sample b/mailserver/config/postfix-main.cf.sample index ea0ec7c..e07e7c3 100644 --- a/mailserver/config/postfix-main.cf.sample +++ b/mailserver/config/postfix-main.cf.sample @@ -32,3 +32,11 @@ smtpd_helo_restrictions = reject_invalid_helo_hostname reject_non_fqdn_helo_hostname reject_unknown_helo_hostname + +mydestination = $myhostname, localhost +transport_maps = regexp:/tmp/docker-mailserver/postfix-transport.cf +smtpd_recipient_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + check_policy_service inet:daemon:10025, + reject_unauth_destination diff --git a/mailserver/config/postfix-transport.cf.sample b/mailserver/config/postfix-transport.cf.sample new file mode 100644 index 0000000..1953ade --- /dev/null +++ b/mailserver/config/postfix-transport.cf.sample @@ -0,0 +1 @@ +/.+@.+/ curl_email: \ No newline at end of file diff --git a/mailserver/config/user-patches.sh.sample b/mailserver/config/user-patches.sh.sample index a5fdd8e..2f2fe84 100644 --- a/mailserver/config/user-patches.sh.sample +++ b/mailserver/config/user-patches.sh.sample @@ -25,5 +25,8 @@ exit 0" >> /usr/local/bin/curl-email.sh chmod +x /usr/local/bin/curl-email.sh +# Add curl_email service to master.cf +postconf -M "curl_email/unix=curl_email unix - n n - - pipe flags=RqX user=nobody argv=/usr/local/bin/curl-email.sh \${sender} \${recipient}" + # reload Postfix to apply changes postfix reload From e0dffbe5fbb584028335e259ebc1b69c2d047b21 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 12:04:27 +0100 Subject: [PATCH 09/22] feat(mailserver): update .gitignore --- mailserver/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/mailserver/.gitignore b/mailserver/.gitignore index 79a4dc3..9975cb1 100644 --- a/mailserver/.gitignore +++ b/mailserver/.gitignore @@ -3,6 +3,7 @@ ################################################# .env +config.json compose.override.yaml docs/site/ docker-data/ From 5352cec559c8d6d54401a96dfb094dbf20dbefe2 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 12:16:55 +0100 Subject: [PATCH 10/22] feat(mailserver): update compose.deploy.yml --- mailserver/compose.deploy.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mailserver/compose.deploy.yml b/mailserver/compose.deploy.yml index 96b28ef..5311fe0 100644 --- a/mailserver/compose.deploy.yml +++ b/mailserver/compose.deploy.yml @@ -57,6 +57,8 @@ services: daemon: image: ${IMAGE_REGISTRY}${DAEMON_IMAGE}:${IMAGE_TAG} container_name: daemon + labels: + - "com.centurylinklabs.watchtower.enable=true" environment: - API_URL=${API_URL} - PSK=${PSK} @@ -66,6 +68,20 @@ services: restart: always networks: net: + + watchtower: + image: containrrr/watchtower + container_name: mailserver-watchtower + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./config.json:/config.json + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_LABEL_ENABLE=true + - WATCHTOWER_POLL_INTERVAL=300 + networks: + - net networks: net: From c7cb2755789d4c5bc6ac0ef9da1dbd18d358a01a Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 12:26:55 +0100 Subject: [PATCH 11/22] feat(mailserver): update compose.deploy.yml --- mailserver/compose.deploy.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mailserver/compose.deploy.yml b/mailserver/compose.deploy.yml index 5311fe0..48cd15b 100644 --- a/mailserver/compose.deploy.yml +++ b/mailserver/compose.deploy.yml @@ -85,8 +85,3 @@ services: networks: net: - driver: bridge - ipam: - config: - - subnet: ${NET_SUBNET} - gateway: ${NET_GATEWAY} From d439f5865e151daf0bc53a1df593cb6a0b370f0e Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 12:30:07 +0100 Subject: [PATCH 12/22] feat(mailserver): update compose.deploy.yml --- mailserver/compose.deploy.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/mailserver/compose.deploy.yml b/mailserver/compose.deploy.yml index 48cd15b..81e6c69 100644 --- a/mailserver/compose.deploy.yml +++ b/mailserver/compose.deploy.yml @@ -57,8 +57,6 @@ services: daemon: image: ${IMAGE_REGISTRY}${DAEMON_IMAGE}:${IMAGE_TAG} container_name: daemon - labels: - - "com.centurylinklabs.watchtower.enable=true" environment: - API_URL=${API_URL} - PSK=${PSK} @@ -68,20 +66,6 @@ services: restart: always networks: net: - - watchtower: - image: containrrr/watchtower - container_name: mailserver-watchtower - restart: unless-stopped - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./config.json:/config.json - environment: - - WATCHTOWER_CLEANUP=true - - WATCHTOWER_LABEL_ENABLE=true - - WATCHTOWER_POLL_INTERVAL=300 - networks: - - net networks: net: From 2093acd50faf9c3eef3be988700dea435f1eed96 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 12:34:01 +0100 Subject: [PATCH 13/22] feat(mailserver): update compose.deploy.yml --- mailserver/.env.sample | 1 + mailserver/compose.deploy.yml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/mailserver/.env.sample b/mailserver/.env.sample index 6bc44cf..3868a50 100644 --- a/mailserver/.env.sample +++ b/mailserver/.env.sample @@ -4,6 +4,7 @@ DAEMON_IMAGE=/mailserver/daemon DMS_HOSTNAME=mail.example.com DMS_IP= +DAEMON_IP= NET_SUBNET= NET_GATEWAY= DOMAIN=example.com diff --git a/mailserver/compose.deploy.yml b/mailserver/compose.deploy.yml index 81e6c69..7072e89 100644 --- a/mailserver/compose.deploy.yml +++ b/mailserver/compose.deploy.yml @@ -66,6 +66,12 @@ services: restart: always networks: net: + ipv4_address: ${DAEMON_IP} networks: net: + driver: bridge + ipam: + config: + - subnet: ${NET_SUBNET} + gateway: ${NET_GATEWAY} From db40fd772c0d156d2e685cad0a4b521a1e309f26 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 13:50:33 +0100 Subject: [PATCH 14/22] feat(mailserver): update daemon/main.go --- mailserver/daemon/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mailserver/daemon/main.go b/mailserver/daemon/main.go index 8b53ef3..451db12 100644 --- a/mailserver/daemon/main.go +++ b/mailserver/daemon/main.go @@ -121,14 +121,14 @@ func handle(conn net.Conn) { // Skip check for local domains if localDomains[domain] { - fmt.Fprintf(conn, "action=OK\n\n") + fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") return } // Check cache if valid, found := checkCache(domain); found { if valid { - fmt.Fprintf(conn, "action=OK\n\n") + fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } @@ -140,7 +140,7 @@ func handle(conn net.Conn) { storeCache(domain, valid) if valid { - fmt.Fprintf(conn, "action=OK\n\n") + fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } From 6f8bbb62b7c0255511e8399892b532c9644a6ca5 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Wed, 18 Mar 2026 14:32:44 +0100 Subject: [PATCH 15/22] feat(mailserver): update postfix-main.cf.sample --- mailserver/config/postfix-main.cf.sample | 21 +++++++++++-------- mailserver/config/postfix-transport.cf.sample | 1 - 2 files changed, 12 insertions(+), 10 deletions(-) delete mode 100644 mailserver/config/postfix-transport.cf.sample diff --git a/mailserver/config/postfix-main.cf.sample b/mailserver/config/postfix-main.cf.sample index e07e7c3..48533ab 100644 --- a/mailserver/config/postfix-main.cf.sample +++ b/mailserver/config/postfix-main.cf.sample @@ -10,6 +10,17 @@ milter_protocol = 6 smtpd_milters = $rspamd_milter non_smtpd_milters = $rspamd_milter +mydestination = $myhostname, localhost +local_recipient_maps = +receive_override_options = no_address_mappings +smtpd_policy_service_timeout = 10s + +smtpd_recipient_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + check_policy_service inet:daemon:10025, + reject_unauth_destination + smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, @@ -22,7 +33,7 @@ smtpd_sender_restrictions = reject_unauth_pipelining smtpd_relay_restrictions = - permit_mynetworks, + permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination @@ -32,11 +43,3 @@ smtpd_helo_restrictions = reject_invalid_helo_hostname reject_non_fqdn_helo_hostname reject_unknown_helo_hostname - -mydestination = $myhostname, localhost -transport_maps = regexp:/tmp/docker-mailserver/postfix-transport.cf -smtpd_recipient_restrictions = - permit_mynetworks, - permit_sasl_authenticated, - check_policy_service inet:daemon:10025, - reject_unauth_destination diff --git a/mailserver/config/postfix-transport.cf.sample b/mailserver/config/postfix-transport.cf.sample deleted file mode 100644 index 1953ade..0000000 --- a/mailserver/config/postfix-transport.cf.sample +++ /dev/null @@ -1 +0,0 @@ -/.+@.+/ curl_email: \ No newline at end of file From d6ed78810840ba76cbd2a9fbc0d49d7c28f65ff1 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 19 Mar 2026 13:27:18 +0100 Subject: [PATCH 16/22] feat(mailserver): update daemon/main.go --- mailserver/daemon/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mailserver/daemon/main.go b/mailserver/daemon/main.go index 451db12..18e156d 100644 --- a/mailserver/daemon/main.go +++ b/mailserver/daemon/main.go @@ -121,14 +121,14 @@ func handle(conn net.Conn) { // Skip check for local domains if localDomains[domain] { - fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") + fmt.Fprintf(conn, "action=OK FILTER curl_email\n\n") return } // Check cache if valid, found := checkCache(domain); found { if valid { - fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") + fmt.Fprintf(conn, "action=OK FILTER curl_email\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } @@ -140,7 +140,7 @@ func handle(conn net.Conn) { storeCache(domain, valid) if valid { - fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") + fmt.Fprintf(conn, "action=OK FILTER curl_email\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } From 91bd8179d2c7b5a0f2e53a053ea82f62ccfb1e2c Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 19 Mar 2026 14:06:48 +0100 Subject: [PATCH 17/22] feat(mailserver): update daemon/main.go --- mailserver/daemon/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mailserver/daemon/main.go b/mailserver/daemon/main.go index 18e156d..1b29ed0 100644 --- a/mailserver/daemon/main.go +++ b/mailserver/daemon/main.go @@ -121,14 +121,14 @@ func handle(conn net.Conn) { // Skip check for local domains if localDomains[domain] { - fmt.Fprintf(conn, "action=OK FILTER curl_email\n\n") + fmt.Fprintf(conn, "action=OK FILTER curl_email:\n\n") return } // Check cache if valid, found := checkCache(domain); found { if valid { - fmt.Fprintf(conn, "action=OK FILTER curl_email\n\n") + fmt.Fprintf(conn, "action=OK FILTER curl_email:\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } @@ -140,7 +140,7 @@ func handle(conn net.Conn) { storeCache(domain, valid) if valid { - fmt.Fprintf(conn, "action=OK FILTER curl_email\n\n") + fmt.Fprintf(conn, "action=OK FILTER curl_email:\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } From 0344dd442ddd1a7e6b0ff63bdb0a062b230658cc Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 19 Mar 2026 14:14:15 +0100 Subject: [PATCH 18/22] feat(mailserver): update daemon/main.go --- mailserver/daemon/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mailserver/daemon/main.go b/mailserver/daemon/main.go index 1b29ed0..451db12 100644 --- a/mailserver/daemon/main.go +++ b/mailserver/daemon/main.go @@ -121,14 +121,14 @@ func handle(conn net.Conn) { // Skip check for local domains if localDomains[domain] { - fmt.Fprintf(conn, "action=OK FILTER curl_email:\n\n") + fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") return } // Check cache if valid, found := checkCache(domain); found { if valid { - fmt.Fprintf(conn, "action=OK FILTER curl_email:\n\n") + fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } @@ -140,7 +140,7 @@ func handle(conn net.Conn) { storeCache(domain, valid) if valid { - fmt.Fprintf(conn, "action=OK FILTER curl_email:\n\n") + fmt.Fprintf(conn, "action=FILTER curl_email:\n\n") } else { fmt.Fprintf(conn, "action=REJECT domain not configured\n\n") } From 13abd34c5af06e48a860948f00a4468dfb211ee8 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 19 Mar 2026 15:32:17 +0100 Subject: [PATCH 19/22] feat(mailserver): update postfix-main.cf.sample --- mailserver/config/postfix-main.cf.sample | 19 ++++++++++--------- .../config/postfix-relay-domains.cf.sample | 1 + 2 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 mailserver/config/postfix-relay-domains.cf.sample diff --git a/mailserver/config/postfix-main.cf.sample b/mailserver/config/postfix-main.cf.sample index 48533ab..ea65ad8 100644 --- a/mailserver/config/postfix-main.cf.sample +++ b/mailserver/config/postfix-main.cf.sample @@ -12,8 +12,14 @@ non_smtpd_milters = $rspamd_milter mydestination = $myhostname, localhost local_recipient_maps = -receive_override_options = no_address_mappings smtpd_policy_service_timeout = 10s +relay_domains = regexp:/tmp/docker-mailserver/postfix-relay-domains.cf + +smtpd_relay_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + permit_auth_destination, + reject_unauth_destination smtpd_recipient_restrictions = permit_mynetworks, @@ -32,14 +38,9 @@ smtpd_sender_restrictions = reject_unknown_sender_domain, reject_unauth_pipelining -smtpd_relay_restrictions = +smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, - reject_unauth_destination - -smtpd_helo_restrictions = - permit_mynetworks - permit_sasl_authenticated - reject_invalid_helo_hostname - reject_non_fqdn_helo_hostname + reject_invalid_helo_hostname, + reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname diff --git a/mailserver/config/postfix-relay-domains.cf.sample b/mailserver/config/postfix-relay-domains.cf.sample new file mode 100644 index 0000000..d7a6d9f --- /dev/null +++ b/mailserver/config/postfix-relay-domains.cf.sample @@ -0,0 +1 @@ +/.+/ OK \ No newline at end of file From eb0c046f9c8225f27c6e7ba820af0b293ec682c5 Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Thu, 19 Mar 2026 15:39:22 +0100 Subject: [PATCH 20/22] feat(service): update domain.go --- api/internal/service/domain.go | 1 - mailserver/config/postfix-virtual.cf.sample | 1 - 2 files changed, 2 deletions(-) delete mode 100644 mailserver/config/postfix-virtual.cf.sample diff --git a/api/internal/service/domain.go b/api/internal/service/domain.go index bc64535..f8da9be 100644 --- a/api/internal/service/domain.go +++ b/api/internal/service/domain.go @@ -85,7 +85,6 @@ func (s *Service) GetVerifiedDomain(ctx context.Context, domainID string, userID func (s *Service) GetVerifiedDomainByName(ctx context.Context, domainName string) (model.Domain, error) { domain, err := s.Store.GetVerifiedDomainByName(ctx, domainName) if err != nil { - log.Printf("error getting verified domain by name: %s", err.Error()) return model.Domain{}, ErrGetDomain } diff --git a/mailserver/config/postfix-virtual.cf.sample b/mailserver/config/postfix-virtual.cf.sample deleted file mode 100644 index 65a3687..0000000 --- a/mailserver/config/postfix-virtual.cf.sample +++ /dev/null @@ -1 +0,0 @@ -@example.net curl_email From ebf8bf47d646e920a0bfda632112b8c6561bb5bf Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 20 Mar 2026 08:08:53 +0100 Subject: [PATCH 21/22] ci: update ci_production.yml --- .github/workflows/ci_production.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci_production.yml b/.github/workflows/ci_production.yml index 85a65c2..120e89d 100644 --- a/.github/workflows/ci_production.yml +++ b/.github/workflows/ci_production.yml @@ -11,6 +11,7 @@ permissions: env: API_IMAGE: email/api APP_IMAGE: email/app + DAEMON_IMAGE: mailserver/daemon TAG: latest REGISTRY: ${{ secrets.REGISTRY_NAME }} @@ -40,6 +41,9 @@ jobs: - name: Build app image run: docker build -t $REGISTRY/$APP_IMAGE:$TAG app/. + + - name: Build daemon image + run: docker build -t $REGISTRY/$DAEMON_IMAGE:$TAG mailserver/daemon/. - name: Log in to registry run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_NAME }} -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin @@ -49,3 +53,6 @@ jobs: - name: Push app:${{ env.TAG }} run: docker push $REGISTRY/$APP_IMAGE:$TAG + + - name: Push daemon:${{ env.TAG }} + run: docker push $REGISTRY/$DAEMON_IMAGE:$TAG \ No newline at end of file From e63ee49456e9b73f9885017ba3a08b9237c970be Mon Sep 17 00:00:00 2001 From: Juraj Hilje Date: Fri, 20 Mar 2026 09:12:37 +0100 Subject: [PATCH 22/22] feat(mailserver): update user-patches.sh.sample --- mailserver/config/user-patches.sh.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailserver/config/user-patches.sh.sample b/mailserver/config/user-patches.sh.sample index 2f2fe84..9ad79c3 100644 --- a/mailserver/config/user-patches.sh.sample +++ b/mailserver/config/user-patches.sh.sample @@ -11,7 +11,7 @@ chmod 640 /etc/sasldb2 # curl-email.sh script echo "#!/bin/sh -curl --silent --fail --data-binary @- -H \"Authorization: Bearer $PSK\" -X POST $API_URL/v1/email +curl --silent --fail --data-binary @- -H \"Authorization: Bearer $PSK\" -X POST http://email-api:3000/v1/email rc=\$?