Dovecot Submission Service for Webmail

This document explains the dedicated dovecot submission service that enables email sending from Roundcube webmail.

The Problem: Webmail SMTP Authentication

When a user composes an email in webmail (Roundcube) and clicks “Send”, the webmail backend needs to submit that email to the SMTP server (postfix). This requires proper authentication to prevent unauthorized email sending.

Mailu’s standard architecture uses the bundled dovecot service in the front container for this purpose, but with TLS_FLAVOR=notls (required to avoid HTTP redirect loops), configuring this bundled dovecot becomes extremely difficult because:

  1. The front container’s dovecot configuration is deeply embedded in Mailu’s startup scripts

  2. Environment variable substitution doesn’t work properly with dovecot syntax

  3. The configuration files are generated at runtime in read-only locations

  4. Modifying the bundled dovecot requires extensive wrapper script modifications

The Solution: Dedicated Dovecot Submission Service

Instead of trying to configure the bundled dovecot in the front container, we deploy a separate dovecot submission service using the official dovecot/dovecot:2.3-latest image with custom configuration.

Architecture

Webmail (Roundcube)
    ↓ (PLAIN auth with token, port 10025)
Dovecot Submission Service
    - Accepts: nopassword=y (static passdb)
    - User: uid=mail (8), gid=mail
    - Mail location: maildir:/tmp/mail
    ↓ (submission_relay_host, no auth, port 25)
Postfix
    - Trusts: mynetworks (10.42.0.0/16 pod network)
    - Accepts: plaintext from pod network
Email Delivery ✅

Key Components

1. Dovecot Submission Service (dovecot-submission-construct.ts)

  • Image: dovecot/dovecot:2.3-latest (official upstream image)

  • Port: 10025 (internal submission with token authentication)

  • Architecture: AMD64 only (official image doesn’t support ARM64)

  • Node Placement: Scheduled to AMD64 node with appropriate nodeSelector and toleration

Configuration highlights:

# Protocols - only submission
protocols = submission

# Allow low UIDs (mail user is UID 8)
first_valid_uid = 8
last_valid_uid = 0

# Mail location (relay-only, no actual storage needed)
mail_location = maildir:/tmp/mail

# Submission relay configuration
submission_relay_host = postfix.mailu.svc.cluster.local
submission_relay_port = 25
submission_relay_trusted = yes
submission_relay_ssl = no

# Authentication via static passdb (token auth handled by webmail, accept all)
passdb {
  driver = static
  args = nopassword=y
}

# User database (static, minimal config for relay)
userdb {
  driver = static
  args = uid=mail gid=mail home=/tmp
}

2. Environment Variable Substitution

Dovecot doesn’t support shell-style ${VAR} syntax natively. To work around this, we use an entrypoint wrapper script that:

  1. Uses sed to substitute placeholders (DOMAIN_PLACEHOLDER, SMTP_ADDRESS_PLACEHOLDER)

  2. Validates the generated configuration with doveconf -c

  3. Starts dovecot with the generated config

3. Service Discovery

The dovecot submission service is registered in the shared ConfigMap as SUBMISSION_ADDRESS:

if (this.dovecotSubmissionConstruct?.service) {
  this.sharedConfigMap.addData(
    'SUBMISSION_ADDRESS',
    `${this.dovecotSubmissionConstruct.service.name}.${namespace}.svc.cluster.local`
  );
}

Webmail uses this environment variable to connect to the correct service (handling CDK8S’s hash-based service names).

Authentication Flow

  1. User sends email from webmail: Roundcube submits to ${SUBMISSION_HOST}:10025 (resolved from SUBMISSION_ADDRESS env var)

  2. Dovecot authenticates: Static passdb with nopassword=y accepts the connection (token validation happens at webmail level, not dovecot)

  3. Dovecot relays to postfix: Using submission_relay_host, dovecot forwards to postfix:25 without authentication (trusted network)

  4. Postfix accepts and delivers: Postfix trusts connections from the pod network (10.42.0.0/16) and delivers the email

Why This Approach Works

No Postfix Authentication Required: Postfix doesn’t need to authenticate dovecot because:

  • Connections come from trusted pod network (mynetworks = 10.42.0.0/16)

  • Only the dovecot submission service can connect to postfix:25 from within the cluster

  • Webmail has already authenticated the user via Mailu’s SSO

Token Authentication at Webmail Level: Roundcube uses Mailu’s session tokens, so by the time a request reaches dovecot submission:

  • User is already authenticated

  • Dovecot just needs to relay (not validate credentials)

  • Static nopassword=y passdb is sufficient

Separate Service Isolation: Using a dedicated service instead of the bundled dovecot:

  • Simplifies configuration (clean dovecot.conf instead of patching Mailu’s templates)

  • Enables easy troubleshooting (dedicated pod with clear logs)

  • Allows independent scaling and resource management

  • Avoids conflicts with Mailu’s internal dovecot usage

Critical Configuration Details

UID Validation

Dovecot’s default first_valid_uid is typically 500 or 1000, but Mailu’s mail user has UID 8. Without first_valid_uid = 8, dovecot rejects logins with:

Mail access for users with UID 8 not permitted (see first_valid_uid in config file)

Mail Storage

Even for relay-only services, dovecot requires mail_location to be set. Without it, errors occur:

mail_location not set and autodetection failed: Mail storage autodetection failed with home=/tmp

We use mail_location = maildir:/tmp/mail as a minimal configuration (no actual mail is stored).

Filesystem Permissions

The container’s /etc/dovecot/ directory is read-only. To work around this:

  1. Mount ConfigMap with templates to /etc/dovecot/config/ (read-only, defaultMode: 0o755 for executable entrypoint)

  2. Generate runtime config in writable /var/run/dovecot/runtime/ directory

  3. Start dovecot with -c /var/run/dovecot/runtime/dovecot.conf

Important: Avoid using /var/run/dovecot/config as the directory name - dovecot has an internal “config” service that conflicts with a directory by that name, causing errors:

service(config): unlink(/run/dovecot/config) failed: Is a directory

AMD64 Architecture Requirement

The official dovecot/dovecot:2.3-latest image only supports AMD64 architecture. For clusters with ARM64 nodes (like kup6s), we need:

Node Selector:

nodeSelector:
  kubernetes.io/arch: amd64

Toleration (for AMD64 taint):

tolerations:
  - key: kubernetes.io/arch
    operator: Equal
    value: amd64
    effect: NoSchedule

This ensures the dovecot submission pod is scheduled to the AMD64 node.

Webmail Configuration

The webmail patch script (webmail-patch-configmap.ts) is updated to use the dovecot submission service:

SUBMISSION_HOST="${SUBMISSION_ADDRESS:-dovecot-submission}"

# Patch SMTP host: Use SUBMISSION_ADDRESS:10025 (dedicated dovecot submission service with token auth)
sed -i "s|tls://[^:]*:10025|smtp://${SUBMISSION_HOST}:10025|g" "$RC_CONFIG"

This replaces the hardcoded FRONT_ADDRESS:10025 reference with the dynamically discovered dovecot submission service.

Troubleshooting

Dovecot pod stuck in CrashLoopBackOff

Check logs:

kubectl logs -n mailu -l app.kubernetes.io/component=dovecot-submission --tail=50

Common issues:

  1. Invalid configuration: Dovecot config validation failed

    • Check for syntax errors in dovecot.conf template

    • Verify environment variable substitution worked correctly

    • Run doveconf -c /var/run/dovecot/runtime/dovecot.conf in the pod

  2. Architecture mismatch: Pod scheduled to ARM64 node

    • Verify nodeSelector and toleration are applied

    • Check node labels: kubectl get nodes --show-labels | grep arch

  3. UID errors: first_valid_uid not set correctly

    • Ensure first_valid_uid = 8 is in the configuration

  4. Mail storage errors: mail_location missing

    • Ensure mail_location = maildir:/tmp/mail is set

Webmail can’t send email (SMTP Error)

Check webmail logs:

kubectl logs -n mailu -l app.kubernetes.io/component=webmail --tail=50 | grep -i smtp

Verify service discovery:

kubectl get configmap -n mailu mailu-shared-config-* -o yaml | grep SUBMISSION_ADDRESS

Should show the full service name (e.g., mailu-dovecot-submission-service-c88c85f9.mailu.svc.cluster.local).

Test connection from webmail pod:

POD=$(kubectl get pod -n mailu -l app.kubernetes.io/component=webmail -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n mailu $POD -- nc -zv mailu-dovecot-submission-service-c88c85f9 10025

Should connect successfully.

Email relay failing at postfix

Check postfix logs:

kubectl logs -n mailu -l app.kubernetes.io/component=postfix --tail=50

Verify postfix trusts pod network:

kubectl exec -n mailu <postfix-pod> -- postconf mynetworks

Should include 10.42.0.0/16 or the cluster’s pod CIDR.

Implementation Files

  • Construct: generic-charts/cdk8s-mailu/src/constructs/dovecot-submission-construct.ts

  • Chart Integration: generic-charts/cdk8s-mailu/src/mailu-chart.ts (createDovecotSubmissionComponent)

  • Webmail Patch: generic-charts/cdk8s-mailu/src/constructs/webmail-patch-configmap.ts

  • Postfix Service: generic-charts/cdk8s-mailu/src/constructs/postfix-construct.ts (added port 10025)