locals { mail_path = "${local.persistent_folder}/mail" mailboxes_path = "${local.mail_path}/mailboxes" getmail_path = "${local.mail_path}/getmail" postfix_path = "${local.mail_path}/postfix" mail_backup_path = "${local.persistent_folder}/backup.mail" email_accounts = [ { domain = "alexpires.me" recipient = "c.alexandre.pires" receive = { type = "imap" server = "mail-pt.securemail.pro" port = 993 } send = { server = "smtp-pt.securemail.pro" port = 465 } username = var.alexpires_me_email_username password = var.alexpires_me_email_password mailboxes = ["INBOX", "Sent", "Drafts", "Trash", "Spam"] } ] getmail_jobs = flatten([ for account in local.email_accounts : [ for mailbox in account.mailboxes : { name = "${account.recipient}-${mailbox}" recipient = "${account.recipient}+${mailbox}" domain = account.domain server = account.receive.server port = account.receive.port username = account.username password = account.password mailbox = mailbox delete_after = try(account.delete_after, 30) } ] ]) postfix_env_vars = { # Email configuration SMTP_HOST = local.scaleway_smtp_host SMTP_PORT = 465 MY_DESTINATION = "alexpires.me" MY_NETWORKS = "127.0.0.0/8" MAILNAME = "smtp.alexpires.me" DOVECOT_AUTH_ADDRESS = "dovecot-auth.mail.svc.cluster.local" DOVECOT_AUTH_PORT = 31000 DOVECOT_LMTP_ADDRESS = "dovecot-lmtp.mail.svc.cluster.local" DOVECOT_LMTP_PORT = 31024 LOG_TO_STDOUT = 1 } postfix_secrets = { SMTP_USER = { key = "username" name = kubernetes_secret.relay_mail_credentials.metadata[0].name } SMTP_PASSWORD = { key = "password" name = kubernetes_secret.relay_mail_credentials.metadata[0].name } } } resource "kubernetes_namespace" "mail" { metadata { annotations = { name = "mail" } labels = { name = "mail" } name = "mail" } } resource "kubernetes_secret" "dovecot_users" { metadata { name = "dovecot-users" namespace = kubernetes_namespace.mail.metadata[0].name } data = { "users" = join("\n", [for account in local.email_accounts : "${account.username}:{plain}${account.password}:1000:1000::/var/mail/${account.username}::"]) } } resource "kubernetes_secret" "relay_mail_credentials" { metadata { name = "relay-mail-credentials" namespace = kubernetes_namespace.mail.metadata[0].name } data = { username = var.scaleway_project_id password = scaleway_iam_api_key.mail_app_api.secret_key } type = "Opaque" } resource "kubernetes_service_account" "mail_app" { metadata { name = "mail-app-sa" namespace = kubernetes_namespace.mail.metadata[0].name } automount_service_account_token = false } resource "kubernetes_config_map" "postfix_config" { metadata { name = "postfix-config" namespace = kubernetes_namespace.mail.metadata[0].name } data = { "run.config" = file("${path.module}/resources/mail/run.config") } } resource "kubernetes_secret" "bcc_map" { metadata { name = "bcc-map" namespace = kubernetes_namespace.mail.metadata[0].name } data = { "bcc_map" = join("\n", [for account in local.email_accounts : "/([^@]+)@${replace(account.domain, ".", "\\.")}/ $${1}+Sent@${account.domain}"]) } } resource "kubernetes_config_map" "getmail_config" { metadata { name = "getmail-config" namespace = kubernetes_namespace.mail.metadata[0].name } data = { for job in local.getmail_jobs : "getmailrc_${job.name}" => templatefile("${path.module}/resources/mail/getmailrc.tpl", { recipient = job.recipient domain = job.domain server = job.server port = job.port username = job.username password = job.password mailbox = job.mailbox delete_after = job.delete_after dovecot_lmtp_address = "dovecot-lmtp.mail.svc.cluster.local" dovecot_lmtp_port = 31024 }) } } resource "kubernetes_config_map" "dovecot_config" { metadata { name = "dovecot-config" namespace = kubernetes_namespace.mail.metadata[0].name } data = { "dovecot.conf" = file("${path.module}/resources/mail/dovecot.conf") } } resource "kubernetes_config_map" "dovecot_sieve" { metadata { name = "dovecot-sieve" namespace = kubernetes_namespace.mail.metadata[0].name } data = { "sieve" = file("${path.module}/resources/mail/sieve") } } resource "kubernetes_manifest" "mail_certificate" { manifest = { apiVersion = "cert-manager.io/v1" kind = "Certificate" metadata = { name = "mail-cert" namespace = kubernetes_namespace.mail.metadata[0].name } spec = { secretName = "mail-tls" issuerRef = { name = "letsencrypt-prod" kind = "ClusterIssuer" } commonName = "imap.${local.domain_name}" dnsNames = ["imap.${local.domain_name}", "smtp.${local.domain_name}"] } } } module "mail_backup" { source = "./modules/scw_backup_job" namespace = kubernetes_namespace.mail.metadata[0].name app_name = "mail" scaleway_organization_id = var.scaleway_organization_id scaleway_project_id = var.scaleway_organization_id access_key = scaleway_iam_api_key.backup_app_api.access_key secret_key = scaleway_iam_api_key.backup_app_api.secret_key bucket_name = scaleway_object_bucket.backup.name retention_days = local.local_backup_retention_days cron_schedule = local.mail_files_backup_cron_schedule source_folder = local.mail_path target_folder = local.mail_backup_path } resource "kubernetes_cron_job_v1" "fetch_emails" { metadata { namespace = kubernetes_namespace.mail.metadata[0].name name = "fetch-emails" } spec { schedule = local.fetch_emails_schedule job_template { metadata { name = "fetch-emails" } spec { backoff_limit = 1 parallelism = 1 completions = 1 active_deadline_seconds = 300 template { metadata { labels = { app = "fetch-emails" } } spec { container { name = "getmail" image = "a13labs/getmail6:${local.getmail_version}" args = concat(["--getmaildir", "/var/getmail"], flatten([for job in local.getmail_jobs : ["-r", "/getmail/getmailrc_${job.name}"]])) volume_mount { name = "getmail-config" mount_path = "/getmail" read_only = true } security_context { run_as_user = 1000 run_as_group = 1000 } volume_mount { name = "getmail-cache" mount_path = "/var/getmail" } } restart_policy = "Never" volume { name = "getmail-config" config_map { name = "getmail-config" } } volume { name = "getmail-cache" host_path { path = local.getmail_path } } } } } } } } resource "kubernetes_deployment" "dovecot" { metadata { namespace = kubernetes_namespace.mail.metadata[0].name name = "dovecot" labels = { app = "dovecot" } } spec { replicas = 1 selector { match_labels = { app = "dovecot" } } strategy { type = "Recreate" } template { metadata { labels = { app = "dovecot" } } spec { service_account_name = kubernetes_service_account.mail_app.metadata[0].name container { name = "dovecot" image = "dovecot/dovecot:latest" port { container_port = 31993 host_port = 993 } port { container_port = 31024 } port { container_port = 31000 } volume_mount { name = "email-storage" mount_path = "/var/mail" } volume_mount { name = "users" mount_path = "/etc/dovecot/users" sub_path = "users" read_only = true } volume_mount { name = "dovecot-config" mount_path = "/etc/dovecot/dovecot.conf" sub_path = "dovecot.conf" read_only = true } volume_mount { name = "dovecot-sieve" mount_path = "/etc/dovecot/sieve/default.sieve" sub_path = "sieve" read_only = true } volume_mount { name = "certs" mount_path = "/etc/letsencrypt" read_only = true } } volume { name = "email-storage" host_path { path = local.mailboxes_path } } volume { name = "users" secret { secret_name = kubernetes_secret.dovecot_users.metadata[0].name } } volume { name = "dovecot-config" config_map { name = kubernetes_config_map.dovecot_config.metadata[0].name } } volume { name = "certs" secret { secret_name = "mail-tls" } } volume { name = "dovecot-sieve" config_map { name = kubernetes_config_map.dovecot_sieve.metadata[0].name } } } } } depends_on = [kubernetes_manifest.mail_certificate, kubernetes_service_account.mail_app] lifecycle { ignore_changes = [spec[0].template[0].metadata[0].annotations] replace_triggered_by = [ kubernetes_config_map.dovecot_config, kubernetes_secret.dovecot_users, kubernetes_config_map.dovecot_sieve, ] } } resource "kubernetes_deployment" "postfix" { metadata { namespace = kubernetes_namespace.mail.metadata[0].name name = "postfix" labels = { app = "postfix" } } spec { replicas = 1 selector { match_labels = { app = "postfix" } } strategy { type = "Recreate" } template { metadata { labels = { app = "postfix" } } spec { service_account_name = kubernetes_service_account.mail_app.metadata[0].name container { name = "postfix" image = "tozd/postfix:alpine-316" port { container_port = 465 host_port = 465 } dynamic "env" { for_each = local.postfix_env_vars content { name = env.key value = env.value } } dynamic "env" { for_each = local.postfix_secrets content { name = env.key value_from { secret_key_ref { key = env.value.key name = env.value.name } } } } volume_mount { name = "email-storage" mount_path = "/var/spool/postfix" } volume_mount { name = "postfix-run-config" mount_path = "/etc/service/postfix/run.config" sub_path = "run.config" read_only = true } volume_mount { name = "bcc-map" mount_path = "/etc/postfix/bcc_map" sub_path = "bcc_map" read_only = true } volume_mount { name = "certs" mount_path = "/etc/letsencrypt" read_only = true } } volume { name = "email-storage" host_path { path = local.postfix_path } } volume { name = "postfix-run-config" config_map { name = kubernetes_config_map.postfix_config.metadata[0].name } } volume { name = "bcc-map" secret { secret_name = kubernetes_secret.bcc_map.metadata[0].name } } volume { name = "certs" secret { secret_name = "mail-tls" } } } } } depends_on = [kubernetes_manifest.mail_certificate, kubernetes_service_account.mail_app] lifecycle { ignore_changes = [spec[0].template[0].metadata[0].annotations] replace_triggered_by = [ kubernetes_config_map.postfix_config, kubernetes_secret.relay_mail_credentials, kubernetes_secret.bcc_map, ] } } resource "kubernetes_service" "dovecot" { metadata { namespace = kubernetes_namespace.mail.metadata[0].name name = "dovecot" } spec { selector = { app = "dovecot" } port { name = "imap" port = 993 target_port = 31993 } type = "LoadBalancer" } lifecycle { ignore_changes = [ metadata[0].annotations ] } } resource "kubernetes_service" "dovecot_lmtp" { metadata { name = "dovecot-lmtp" namespace = kubernetes_namespace.mail.metadata[0].name } spec { selector = { app = "dovecot" } port { port = 31024 } cluster_ip = "None" } } resource "kubernetes_service" "dovecot_auth" { metadata { name = "dovecot-auth" namespace = kubernetes_namespace.mail.metadata[0].name } spec { selector = { app = "dovecot" } port { port = 31000 } cluster_ip = "None" } } resource "kubernetes_service" "postfix" { metadata { namespace = kubernetes_namespace.mail.metadata[0].name name = "postfix" } spec { selector = { app = "postfix" } port { name = "smtp" port = 465 target_port = 465 } type = "LoadBalancer" } lifecycle { ignore_changes = [ metadata[0].annotations ] } }