Implement mail server

This commit is contained in:
2025-03-15 23:01:46 +01:00
parent d8011643be
commit 98f789d2c8
16 changed files with 703 additions and 9 deletions
+42
View File
@@ -118,3 +118,45 @@ resource "scaleway_object_bucket_policy" "nextcloud" {
]
})
}
resource "scaleway_object_bucket" "dovecot" {
name = "a13labs-dovecot-${random_string.bucket_suffix.result}"
project_id = var.scaleway_project_id
region = "fr-par"
}
resource "scaleway_object_bucket_policy" "dovecot" {
bucket = scaleway_object_bucket.dovecot.name
policy = jsonencode({
Version = "2023-04-17",
Id = "AllowAccess",
Statement = [
{
Effect = "Allow"
Action = [
"s3:*",
]
Principal = {
SCW = "user_id:${var.scaleway_user_id}"
}
Resource = [
scaleway_object_bucket.dovecot.name,
"${scaleway_object_bucket.dovecot.name}/*",
]
},
{
Effect = "Allow"
Action = [
"s3:*",
]
Principal = {
SCW = "application_id:${scaleway_iam_application.dovecot_app.id}"
}
Resource = [
scaleway_object_bucket.dovecot.name,
"${scaleway_object_bucket.dovecot.name}/*",
]
},
]
})
}
+45 -5
View File
@@ -1,18 +1,20 @@
locals {
domain_name = "alexpires.me"
# apps
wordpress_version = "6.7.2"
wordpress_fqdns = ["blog.alexpires.me", "www.alexpires.me", "alexpires.me"]
wordpress_fqdns = ["blog.${local.domain_name}", "www.${local.domain_name}", "${local.domain_name}"]
wordpress_sql_backup_cron_schedule = "0 2 * * *"
wordpress_files_backup_cron_schedule = "10 2 * * *"
gitea_version = "1.23.4"
gitea_fqdn = "code.alexpires.me"
gitea_fqdn = "code.${local.domain_name}"
gitea_sql_backup_cron_schedule = "20 2 * * *"
gitea_files_backup_cron_schedule = "30 2 * * *"
nextcloud_version = "30.0.0-apache"
nextcloud_fqdn = "cloud.alexpires.me"
nextcloud_fqdn = "cloud.${local.domain_name}"
nextcloud_sql_backup_cron_schedule = "40 2 * * *"
nextcloud_files_backup_cron_schedule = "50 2 * * *"
@@ -27,7 +29,7 @@ locals {
m3uproxy_version = "latest"
wireguard_version = "1.0.20210914-r4-ls54"
squid_version = "6.1-23.10_edge"
m3uproxy_fqdn = "tv.alexpires.me"
m3uproxy_fqdn = "tv.${local.domain_name}"
geoip_image = "maxmindinc/geoipupdate"
geoip_version = "latest"
@@ -40,7 +42,7 @@ locals {
# rustdesk
rustdesk_version = "latest"
rustdesk_fqdn = "rustdesk.alexpires.me"
rustdesk_fqdn = "rustdesk.${local.domain_name}"
# backup
local_backup_retention_days = 2
@@ -50,6 +52,44 @@ locals {
scaleway_smtp_host = "smtp.tem.scw.cloud"
scaleway_smtp_port = 587
# mail settings
getmail_version = "v0.0.1-6.19.07"
fetch_emails_schedule = "*/2 * * * *"
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)
}
]
])
# storage
persistent_folder = "/mnt/microk8s_persistent"
database_folder = "/var/lib"
+26
View File
@@ -10,6 +10,10 @@ resource "scaleway_iam_application" "nextcloud_app" {
name = "nextcloud_app"
}
resource "scaleway_iam_application" "dovecot_app" {
name = "dovecot_app"
}
resource "scaleway_iam_policy" "object_read_write_backup" {
name = "K8s backup app policy"
description = "Gives k8s backup job access to object storage in project"
@@ -56,6 +60,23 @@ resource "scaleway_iam_policy" "object_read_write_nextcloud" {
}
}
resource "scaleway_iam_policy" "object_read_write_dovecot" {
name = "Dovecot app policy"
description = "Gives dovecot job access to object storage in project"
application_id = scaleway_iam_application.dovecot_app.id
rule {
project_ids = [data.scaleway_account_project.default.id]
permission_set_names = [
"ObjectStorageObjectsWrite",
"ObjectStorageObjectsRead",
"ObjectStorageBucketsRead",
"ObjectStorageBucketsWrite",
"ObjectStorageObjectsDelete"
]
}
}
resource "scaleway_iam_api_key" "backup_app_api" {
application_id = scaleway_iam_application.backup_app.id
description = "k8s backup app API key"
@@ -70,3 +91,8 @@ resource "scaleway_iam_api_key" "nextcloud_app_api" {
application_id = scaleway_iam_application.nextcloud_app.id
description = "nextcloud access API key"
}
resource "scaleway_iam_api_key" "dovecot_app_api" {
application_id = scaleway_iam_application.dovecot_app.id
description = "dovecot access API key"
}
-4
View File
@@ -16,7 +16,6 @@ locals {
m3uproxy_config = templatefile("${path.module}/resources/m3uproxy/m3uproxy.json", {
m3uproxy_secret = var.m3uproxy_secret
})
}
resource "kubernetes_namespace" "m3uproxy" {
@@ -24,14 +23,11 @@ resource "kubernetes_namespace" "m3uproxy" {
annotations = {
name = "m3uproxy"
}
labels = {
name = "m3uproxy"
}
name = "m3uproxy"
}
}
resource "kubernetes_secret" "m3uproxy_admin_credentials" {
+320
View File
@@ -0,0 +1,320 @@
locals {
mail_home = "${local.persistent_folder}/mail"
mailboxes_path = "${local.mail_home}/mailboxes"
getmail_path = "${local.mail_home}/getmail"
}
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_service_account" "mail_service_account" {
metadata {
name = "mail-app-sa"
namespace = kubernetes_namespace.mail.metadata[0].name
}
automount_service_account_token = false
}
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}"]
}
}
}
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" "mail" {
metadata {
namespace = kubernetes_namespace.mail.metadata[0].name
name = "mail"
labels = {
app = "mail"
}
}
spec {
replicas = 1
selector {
match_labels = {
app = "mail"
}
}
template {
metadata {
labels = {
app = "mail"
}
}
spec {
service_account_name = kubernetes_service_account.mail_service_account.metadata[0].name
container {
name = "dovecot"
image = "dovecot/dovecot:latest"
port {
container_port = 31993
host_port = 993
}
port {
container_port = 31024
}
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_service_account]
lifecycle {
replace_triggered_by = [
kubernetes_config_map.dovecot_config,
kubernetes_secret.dovecot_users,
kubernetes_config_map.dovecot_sieve,
]
}
}
resource "kubernetes_service" "dovecot" {
metadata {
namespace = kubernetes_namespace.mail.metadata[0].name
name = "mail"
}
spec {
selector = {
app = "mail"
}
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 = "mail"
}
port {
port = 31024
}
cluster_ip = "None"
}
}
@@ -0,0 +1,239 @@
dovecot_config_version = "2.4.0"
dovecot_storage_version = "2.4.0"
base_dir = /run/dovecot
state_dir = /run/dovecot
protocols = imap lmtp
listen = *
mail_driver=maildir
mailbox_list_layout=index
mailbox_list_utf8=yes
mail_path=~/mail
mail_home=/var/mail/%{user|lower}
mail_utf8_extensions = yes
default_internal_user = vmail
default_login_user = vmail
default_internal_group = vmail
mail_uid = vmail
mail_gid = vmail
namespace inbox {
inbox = yes
mailbox Drafts {
special_use = \Drafts
auto = create
auto = subscribe
}
mailbox Spam {
special_use = \Spam
auto = create
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = create
auto = subscribe
}
mailbox Sent {
special_use = \Sent
auto = create
auto = subscribe
}
mailbox Archive {
special_use = \Archive
auto = create
auto = subscribe
}
mailbox Important {
special_use = \Important
comment = All my important messages
auto = create
auto = subscribe
}
}
passdb passwd-file {
driver = passwd-file
passwd_file_path = /etc/dovecot/users
}
ssl_server {
cert_file = /etc/letsencrypt/tls.crt
key_file = /etc/letsencrypt/tls.key
}
mail_attribute {
dict file {
path = %{home}/dovecot-attributes
}
}
log_path = /dev/stdout
imap_hibernate_timeout = 5s
mail_plugins {
fts = yes
fts_flatcurve = yes
mail_log = yes
notify = yes
}
mail_log_events = delete undelete expunge save copy mailbox_create mailbox_delete mailbox_rename flag_change
fts_autoindex = yes
fts_autoindex_max_recent_msgs = 999
fts_search_add_missing = yes
language_filters = normalizer-icu snowball stopwords
language_tokenizers = generic email-address
language_tokenizer_generic_algorithm = simple
language en {
default = yes
filters = lowercase snowball english-possessive stopwords
}
fts flatcurve {
substring_search = yes
}
protocol imap {
mail_plugins {
imap_sieve = yes
imap_filter_sieve = yes
}
}
lmtp_save_to_detail_mailbox = yes
recipient_delimiter = +
protocol lmtp {
mail_plugins {
sieve = yes
}
}
service imap-login {
process_min_avail = 1
client_limit = 100
inet_listener imap {
port = 31143
}
inet_listener imaps {
port = 31993
ssl = yes
}
}
service stats {
process_min_avail = 1
inet_listener http {
port = 9090
}
}
service auth {
inet_listener auth {
port = 31000
}
}
# LMTP service to store sent emails
service lmtp {
inet_listener auth {
port = 31024
}
mail_plugins {
sieve = yes
}
}
event_exporter log {
format = json
time_format = rfc3339
}
metric auth_success {
filter = (event=auth_request_finished AND success=yes)
}
metric auth_failure {
filter = (event=auth_request_finished AND NOT success=yes)
exporter = log
}
metric imap_command {
filter = event=imap_command_finished
group_by cmd_name {
method discrete {
}
}
group_by tagged_reply_state {
method discrete {
}
}
}
metric smtp_command {
filter = event=smtp_server_command_finished and protocol=submission
group_by cmd_name {
method discrete {
}
}
group_by status_code {
method discrete {
}
}
group_by duration {
method exponential {
base = 10
min_magnitude = 1
max_magnitude = 5
}
}
}
metric lmtp_command {
filter = event=smtp_server_command_finished and protocol=lmtp
group_by cmd_name {
method discrete {
}
}
group_by status_code {
method discrete {
}
}
group_by duration {
method exponential {
base = 10
min_magnitude = 1
max_magnitude = 5
}
}
}
metric mail_delivery {
filter = event=mail_delivery_finished
group_by duration {
method exponential {
base = 10
min_magnitude = 1
max_magnitude = 5
}
}
}
sieve_script default {
path = /etc/dovecot/sieve/default.sieve
}
@@ -0,0 +1,19 @@
[retriever]
type = SimpleIMAPSSLRetriever
server = ${server}
port = ${port}
username = ${username}
password = ${password}
mailboxes = ('${mailbox}',)
[destination]
type = MDA_lmtp
host = ${dovecot_lmtp_address}
port = ${dovecot_lmtp_port}
override = ${recipient}@${domain}
[options]
read_all = true
delete_after = ${delete_after}
verbose = 1
+2
View File
@@ -19,3 +19,5 @@ TF_VAR_maxmind_account_id=$MAXMIND_ACCOUNT_ID
TF_VAR_maxmind_license_key=$MAXMIND_LICENSE_KEY
TF_VAR_rustdesk_private_key=$RUSTDESK_PRIVATE_KEY
TF_VAR_rustdesk_public_key=$RUSTDESK_PUBLIC_KEY
TF_VAR_alexpires_me_email_username=$ALEXPIRES_ME_EMAIL_USERNAME
TF_VAR_alexpires_me_email_password=$ALEXPIRES_ME_EMAIL_PASSWORD
+10
View File
@@ -89,3 +89,13 @@ variable "rustdesk_public_key" {
description = "Rustdesk public key"
type = string
}
variable "alexpires_me_email_username" {
description = "POP3 username"
type = string
}
variable "alexpires_me_email_password" {
description = "POP3 password"
type = string
}