Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b9fff8c
feat(api): update domain.go
jurajhilje Mar 13, 2026
b11ac00
feat(mailserver): create daemon/main.go
jurajhilje Mar 16, 2026
bb70134
feat(mailserver): update compose.yml
jurajhilje Mar 16, 2026
6d7d0aa
feat(mailserver): update compose.yml
jurajhilje Mar 16, 2026
fbd9354
ci: update ci_staging.yml
jurajhilje Mar 16, 2026
06d3844
feat(mailserver): update .env.sample
jurajhilje Mar 16, 2026
df528db
feat(mailserver): update compose.deploy.yml
jurajhilje Mar 17, 2026
6e6087f
feat(mailserver): create postfix-transport.cf.sample
jurajhilje Mar 18, 2026
e0dffbe
feat(mailserver): update .gitignore
jurajhilje Mar 18, 2026
5352cec
feat(mailserver): update compose.deploy.yml
jurajhilje Mar 18, 2026
c7cb275
feat(mailserver): update compose.deploy.yml
jurajhilje Mar 18, 2026
d439f58
feat(mailserver): update compose.deploy.yml
jurajhilje Mar 18, 2026
2093acd
feat(mailserver): update compose.deploy.yml
jurajhilje Mar 18, 2026
db40fd7
feat(mailserver): update daemon/main.go
jurajhilje Mar 18, 2026
6f8bbb6
feat(mailserver): update postfix-main.cf.sample
jurajhilje Mar 18, 2026
d6ed788
feat(mailserver): update daemon/main.go
jurajhilje Mar 19, 2026
91bd817
feat(mailserver): update daemon/main.go
jurajhilje Mar 19, 2026
0344dd4
feat(mailserver): update daemon/main.go
jurajhilje Mar 19, 2026
13abd34
feat(mailserver): update postfix-main.cf.sample
jurajhilje Mar 19, 2026
eb0c046
feat(service): update domain.go
jurajhilje Mar 19, 2026
ebf8bf4
ci: update ci_production.yml
jurajhilje Mar 20, 2026
e63ee49
feat(mailserver): update user-patches.sh.sample
jurajhilje Mar 20, 2026
5993a10
Merge branch 'feature/custom-domains' into feature/custom-domains-dms
jurajhilje Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci_production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ permissions:
env:
API_IMAGE: email/api
APP_IMAGE: email/app
DAEMON_IMAGE: mailserver/daemon
TAG: latest
REGISTRY: ${{ secrets.REGISTRY_NAME }}

Expand Down Expand Up @@ -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
Expand All @@ -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
7 changes: 7 additions & 0 deletions .github/workflows/ci_staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ permissions:
env:
API_IMAGE: email/api
APP_IMAGE: email/app
DAEMON_IMAGE: mailserver/daemon
TAG: staging
REGISTRY: ${{ secrets.REGISTRY_NAME }}

Expand Down Expand Up @@ -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
Expand All @@ -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
6 changes: 6 additions & 0 deletions api/internal/repository/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions api/internal/service/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -81,6 +82,15 @@ 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 {
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 {
Expand Down
41 changes: 41 additions & 0 deletions api/internal/transport/api/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
1 change: 1 addition & 0 deletions api/internal/transport/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
7 changes: 7 additions & 0 deletions mailserver/.env.sample
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
IMAGE_REGISTRY=
IMAGE_TAG=latest
DAEMON_IMAGE=/mailserver/daemon

DMS_HOSTNAME=mail.example.com
DMS_IP=
DAEMON_IP=
NET_SUBNET=
NET_GATEWAY=
DOMAIN=example.com
DOMAINS=example.com,example.net
SMTP_USER=
SMTP_PASS=
SSL_TYPE=
ENABLE_RSPAMD=1
ENABLE_FAIL2BAN=1
ENABLE_CLAMAV=0
LOG_LEVEL=info
API_URL=http://email-api:3000
PSK=""
1 change: 1 addition & 0 deletions mailserver/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#################################################

.env
config.json
compose.override.yaml
docs/site/
docker-data/
Expand Down
14 changes: 14 additions & 0 deletions mailserver/compose.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ 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:
ipv4_address: ${DAEMON_IP}

networks:
net:
driver: bridge
Expand Down
13 changes: 13 additions & 0 deletions mailserver/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ services:
retries: 0
networks:
email_net:

daemon:
build: ./daemon
container_name: daemon
environment:
- API_URL=${API_URL}
- PSK=${PSK}
- LOCAL_DOMAINS=${DOMAINS}
- CACHE_POSITIVE_TTL=10m
- CACHE_NEGATIVE_TTL=2m
restart: always
networks:
email_net:

networks:
email_net:
Expand Down
30 changes: 21 additions & 9 deletions mailserver/config/postfix-main.cf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ milter_protocol = 6
smtpd_milters = $rspamd_milter
non_smtpd_milters = $rspamd_milter

mydestination = $myhostname, localhost
local_recipient_maps =
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,
permit_sasl_authenticated,
check_policy_service inet:daemon:10025,
reject_unauth_destination

smtpd_sender_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
Expand All @@ -21,14 +38,9 @@ smtpd_sender_restrictions =
reject_unknown_sender_domain,
reject_unauth_pipelining

smtpd_relay_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
permit_mynetworks,
permit_sasl_authenticated,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
reject_unknown_helo_hostname
1 change: 1 addition & 0 deletions mailserver/config/postfix-relay-domains.cf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.+/ OK
1 change: 0 additions & 1 deletion mailserver/config/postfix-virtual.cf.sample

This file was deleted.

3 changes: 3 additions & 0 deletions mailserver/config/user-patches.sh.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions mailserver/daemon/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
3 changes: 3 additions & 0 deletions mailserver/daemon/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module daemon

go 1.24
Loading