This commit is contained in:
traveler 2025-04-17 17:29:05 -05:00
commit 5aa7d034f7
3292 changed files with 465160 additions and 0 deletions

47
data/conf/clamav/clamd.conf Executable file
View file

@ -0,0 +1,47 @@
#Debug true
#LogFile /dev/null
LogTime yes
LogClean yes
ExtendedDetectionInfo yes
PidFile /run/clamav/clamd.pid
OfficialDatabaseOnly no
LocalSocket /run/clamav/clamd.sock
TCPSocket 3310
StreamMaxLength 25M
MaxThreads 10
ReadTimeout 10
CommandReadTimeout 3
SendBufTimeout 200
MaxQueue 80
IdleTimeout 20
SelfCheck 3600
User clamav
Foreground yes
DetectPUA yes
# See https://github.com/vrtadmin/clamav-faq/blob/master/faq/faq-pua.md
#ExcludePUA NetTool
#ExcludePUA PWTool
#IncludePUA Spy
#IncludePUA Scanner
#IncludePUA RAT
HeuristicAlerts yes
ScanOLE2 yes
AlertOLE2Macros no
ScanPDF yes
ScanSWF yes
ScanXMLDOCS yes
ScanHWP3 yes
ScanMail yes
PhishingSignatures no
PhishingScanURLs no
HeuristicScanPrecedence yes
ScanHTML yes
ScanArchive yes
MaxScanSize 50M
MaxFileSize 25M
MaxRecursion 5
MaxFiles 200
Bytecode yes
BytecodeSecurity TrustSigned
BytecodeTimeout 1000
ConcurrentDatabaseReload no

19
data/conf/clamav/freshclam.conf Executable file
View file

@ -0,0 +1,19 @@
#UpdateLogFile /dev/console
LogTime yes
PidFile /run/clamav/freshclam.pid
DatabaseOwner clamav
DNSDatabaseInfo current.cvd.clamav.net
DatabaseMirror db.uk.clamav.net
DatabaseMirror db.nl.clamav.net
DatabaseMirror db.fr.clamav.net
DatabaseMirror db.ch.clamav.net
MaxAttempts 4
ScriptedUpdates yes
Checks 6
NotifyClamd /etc/clamav/clamd.conf
Foreground yes
ConnectTimeout 20
ReceiveTimeout 20
TestDatabases yes
Bytecode yes

View file

@ -0,0 +1,14 @@
# Autogenerated by mailcow
plugin {
fts = solr
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_autoindex_exclude2 = \Trash
fts_solr = url=http://solr:8983/solr/dovecot-fts/
fts_tokenizers = generic email-address
fts_tokenizer_generic = algorithm=simple
fts_filters = normalizer-icu snowball stopwords
fts_filters_en = lowercase snowball english-possessive stopwords
}

309
data/conf/dovecot/dovecot.conf Executable file
View file

@ -0,0 +1,309 @@
# --------------------------------------------------------------------------
# Please create a file "extra.conf" for persistent overrides to dovecot.conf
# --------------------------------------------------------------------------
# LDAP example:
#passdb {
# args = /etc/dovecot/ldap/passdb.conf
# driver = ldap
#}
auth_mechanisms = plain login
#mail_debug = yes
#auth_debug = yes
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
log_path = syslog
disable_plaintext_auth = yes
# Uncomment on NFS share
#mmap_disable = yes
#mail_fsync = always
#mail_nfs_index = yes
#mail_nfs_storage = yes
login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
mail_home = /var/vmail/%d/%n
mail_location = maildir:~/
mail_plugins = </etc/dovecot/mail_plugins
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
mail_attachment_dir = /var/attachments
mail_attachment_min_size = 128k
# Significantly speeds up very large mailboxes, but is only safe to enable if
# you do not manually modify the files in the `cur` directories in
# mailcowdockerized_vmail-vol-1.
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
maildir_very_dirty_syncs = yes
# Dovecot 2.2
#ssl_protocols = !SSLv3
# Dovecot 2.3
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
# Default in Dovecot 2.3
ssl_options = no_compression no_ticket
# New in Dovecot 2.3
ssl_dh = </etc/ssl/mail/dhparams.pem
# Dovecot 2.2
#ssl_dh_parameters_length = 2048
log_timestamp = "%Y-%m-%d %H:%M:%S "
recipient_delimiter = +
auth_master_user_separator = *
mail_shared_explicit_inbox = yes
mail_prefetch_count = 30
passdb {
driver = lua
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
result_success = return-ok
result_failure = continue
result_internalfail = continue
}
# try a master passwd
passdb {
driver = passwd-file
args = /etc/dovecot/dovecot-master.passwd
master = yes
skip = authenticated
}
# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
# a return of the following passdb is mandatory
passdb {
driver = lua
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
}
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
service doveadm {
inet_listener {
port = 12345
}
vsz_limit=2048 MB
}
!include /etc/dovecot/dovecot.folders.conf
protocols = imap sieve lmtp pop3
service dict {
unix_listener dict {
mode = 0660
user = vmail
group = vmail
}
}
service log {
user = dovenull
}
service config {
unix_listener config {
user = root
group = vmail
mode = 0660
}
}
service auth {
inet_listener auth-inet {
port = 10001
}
unix_listener auth-master {
mode = 0600
user = vmail
}
unix_listener auth-userdb {
mode = 0600
user = vmail
}
vsz_limit = 2G
}
service managesieve-login {
inet_listener sieve {
port = 4190
}
inet_listener sieve_haproxy {
port = 14190
haproxy = yes
}
service_count = 1
process_min_avail = 2
vsz_limit = 1G
}
service imap-login {
service_count = 1
process_limit = 10000
vsz_limit = 1G
user = dovenull
inet_listener imap_haproxy {
port = 10143
haproxy = yes
}
inet_listener imaps_haproxy {
port = 10993
ssl = yes
haproxy = yes
}
}
service pop3-login {
service_count = 1
vsz_limit = 1G
inet_listener pop3_haproxy {
port = 10110
haproxy = yes
}
inet_listener pop3s_haproxy {
port = 10995
ssl = yes
haproxy = yes
}
}
service imap {
executable = imap
user = vmail
vsz_limit = 1G
}
service managesieve {
process_limit = 256
}
service lmtp {
inet_listener lmtp-inet {
port = 24
}
user = vmail
}
listen = *,[::]
ssl_cert = </etc/ssl/mail/cert.pem
ssl_key = </etc/ssl/mail/key.pem
userdb {
driver = passwd-file
args = /etc/dovecot/dovecot-master.userdb
}
userdb {
args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
driver = sql
skip = found
}
protocol imap {
mail_plugins = </etc/dovecot/mail_plugins_imap
imap_metadata = yes
}
mail_attribute_dict = file:%h/dovecot-attributes
protocol lmtp {
mail_plugins = </etc/dovecot/mail_plugins_lmtp
auth_socket_path = /var/run/dovecot/auth-master
}
protocol sieve {
managesieve_logout_format = bytes=%i/%o
}
plugin {
# Allow "any" or "authenticated" to be used in ACLs
acl_anyone = </etc/dovecot/acl_anyone
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
acl = vfile
acl_user = %u
quota = dict:Userquota::proxy::sqlquota
quota_rule2 = Trash:storage=+100%%
sieve = /var/vmail/sieve/%u.sieve
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_vacation_send_from_recipient = yes
sieve_redirect_envelope_from = recipient
# From elsewhere to Spam folder
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
# END
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
# END
master_user = %u
quota_warning = storage=95%% quota-warning 95 %u
quota_warning2 = storage=80%% quota-warning 80 %u
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
sieve_extensions = +notify +imapflags +vacation-seconds +editheader
sieve_max_script_size = 1M
sieve_max_redirects = 100
sieve_max_actions = 101
sieve_quota_max_scripts = 0
sieve_quota_max_storage = 0
listescape_char = "\\"
sieve_vacation_min_period = 5s
sieve_vacation_max_period = 0
sieve_vacation_default_period = 60s
sieve_before = /var/vmail/sieve/global_sieve_before.sieve
sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve
sieve_duplicate_default_period = 1m
sieve_duplicate_max_period = 7d
# -- Global keys
mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
mail_crypt_save_version = 2
# Enable compression while saving, lz4 Dovecot v2.2.11+
zlib_save = lz4
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size
mail_log_cached_only = yes
# Try set mail_replica
!include_try /etc/dovecot/mail_replica.conf
}
service quota-warning {
executable = script /usr/local/bin/quota_notify.py
# use some unprivileged user for executing the quota warnings
user = vmail
unix_listener quota-warning {
user = vmail
}
}
dict {
sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf
sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
}
remote 127.0.0.1 {
disable_plaintext_auth = no
}
submission_host = postfix:588
mail_max_userip_connections = 500
service stats {
unix_listener stats-writer {
mode = 0660
user = vmail
}
}
imap_max_line_length = 2 M
#auth_cache_verify_password_with_worker = yes
#auth_cache_negative_ttl = 0
#auth_cache_ttl = 30 s
#auth_cache_size = 2 M
service replicator {
process_min_avail = 1
}
service aggregator {
fifo_listener replication-notify-fifo {
user = vmail
}
unix_listener replication-notify {
user = vmail
}
}
service replicator {
unix_listener replicator-doveadm {
mode = 0666
}
}
replication_max_conns = 10
doveadm_port = 12345
replication_dsync_parameters = -d -l 30 -U -n INBOX
# <Includes>
!include_try /etc/dovecot/sni.conf
!include_try /etc/dovecot/sogo_trusted_ip.conf
!include_try /etc/dovecot/extra.conf
!include_try /etc/dovecot/sogo-sso.conf
!include_try /etc/dovecot/shared_namespace.conf
!include_try /etc/dovecot/conf.d/fts.conf
# </Includes>
default_client_limit = 10400
default_vsz_limit = 1024 M

View file

@ -0,0 +1,308 @@
namespace inbox {
inbox = yes
location =
separator = /
mailbox "Trash" {
auto = subscribe
special_use = \Trash
}
mailbox "Deleted Messages" {
special_use = \Trash
}
mailbox "Deleted Items" {
special_use = \Trash
}
mailbox "Rubbish" {
special_use = \Trash
}
mailbox "Gelöschte Objekte" {
special_use = \Trash
}
mailbox "Gelöschte Elemente" {
special_use = \Trash
}
mailbox "Papierkorb" {
special_use = \Trash
}
mailbox "Itens Excluidos" {
special_use = \Trash
}
mailbox "Itens Excluídos" {
special_use = \Trash
}
mailbox "Lixeira" {
special_use = \Trash
}
mailbox "Prullenbak" {
special_use = \Trash
}
mailbox "Odstránené položky" {
special_use = \Trash
}
mailbox "Koš" {
special_use = \Trash
}
mailbox "Verwijderde items" {
special_use = \Trash
}
mailbox "Удаленные" {
special_use = \Trash
}
mailbox "Удаленные элементы" {
special_use = \Trash
}
mailbox "Корзина" {
special_use = \Trash
}
mailbox "Видалені" {
special_use = \Trash
}
mailbox "Видалені елементи" {
special_use = \Trash
}
mailbox "Кошик" {
special_use = \Trash
}
mailbox "废件箱" {
special_use = \Trash
}
mailbox "已删除消息" {
special_use = \Trash
}
mailbox "已删除邮件" {
special_use = \Trash
}
mailbox "Archive" {
auto = subscribe
special_use = \Archive
}
mailbox "Archiv" {
special_use = \Archive
}
mailbox "Archives" {
special_use = \Archive
}
mailbox "Arquivo" {
special_use = \Archive
}
mailbox "Arquivos" {
special_use = \Archive
}
mailbox "Archief" {
special_use = \Archive
}
mailbox "Archív" {
special_use = \Archive
}
mailbox "Archivovať" {
special_use = \Archive
}
mailbox "归档" {
special_use = \Archive
}
mailbox "Архив" {
special_use = \Archive
}
mailbox "Архів" {
special_use = \Archive
}
mailbox "Sent" {
auto = subscribe
special_use = \Sent
}
mailbox "Sent Messages" {
special_use = \Sent
}
mailbox "Sent Items" {
special_use = \Sent
}
mailbox "已发送" {
special_use = \Sent
}
mailbox "已发送消息" {
special_use = \Sent
}
mailbox "已发送邮件" {
special_use = \Sent
}
mailbox "Отправленные" {
special_use = \Sent
}
mailbox "Отправленные элементы" {
special_use = \Sent
}
mailbox "Надіслані" {
special_use = \Sent
}
mailbox "Надіслані елементи" {
special_use = \Sent
}
mailbox "Gesendet" {
special_use = \Sent
}
mailbox "Gesendete Objekte" {
special_use = \Sent
}
mailbox "Gesendete Elemente" {
special_use = \Sent
}
mailbox "Itens Enviados" {
special_use = \Sent
}
mailbox "Enviados" {
special_use = \Sent
}
mailbox "Verzonden items" {
special_use = \Sent
}
mailbox "Verzonden" {
special_use = \Sent
}
mailbox "Odoslaná pošta" {
special_use = \Sent
}
mailbox "Odoslané" {
special_use = \Sent
}
mailbox "Drafts" {
auto = subscribe
special_use = \Drafts
}
mailbox "Entwürfe" {
special_use = \Drafts
}
mailbox "Rascunhos" {
special_use = \Drafts
}
mailbox "Concepten" {
special_use = \Drafts
}
mailbox "Koncepty" {
special_use = \Drafts
}
mailbox "草稿" {
special_use = \Drafts
}
mailbox "草稿箱" {
special_use = \Drafts
}
mailbox "Черновики" {
special_use = \Drafts
}
mailbox "Чернетки" {
special_use = \Drafts
}
mailbox "Junk" {
auto = subscribe
special_use = \Junk
}
mailbox "Junk-E-Mail" {
special_use = \Junk
}
mailbox "Junk E-Mail" {
special_use = \Junk
}
mailbox "Spam" {
special_use = \Junk
}
mailbox "Lixo Eletrônico" {
special_use = \Junk
}
mailbox "Nevyžiadaná pošta" {
special_use = \Junk
}
mailbox "Infikované položky" {
special_use = \Junk
}
mailbox "Ongewenste e-mail" {
special_use = \Junk
}
mailbox "垃圾" {
special_use = \Junk
}
mailbox "垃圾箱" {
special_use = \Junk
}
mailbox "Нежелательная почта" {
special_use = \Junk
}
mailbox "Спам" {
special_use = \Junk
}
mailbox "Небажана пошта" {
special_use = \Junk
}
mailbox "Koncepty" {
special_use = \Drafts
}
mailbox "Nevyžádaná pošta" {
special_use = \Junk
}
mailbox "Odstraněná pošta" {
special_use = \Trash
}
mailbox "Odeslaná pošta" {
special_use = \Sent
}
mailbox "Skräp" {
special_use = \Trash
}
mailbox "Borttagna Meddelanden" {
special_use = \Trash
}
mailbox "Arkiv" {
special_use = \Archive
}
mailbox "Arkeverat" {
special_use = \Archive
}
mailbox "Skickat" {
special_use = \Sent
}
mailbox "Skickade Meddelanden" {
special_use = \Sent
}
mailbox "Utkast" {
special_use = \Drafts
}
mailbox "Skraldespand" {
special_use = \Trash
}
mailbox "Slettet mails" {
special_use = \Trash
}
mailbox "Arkiv" {
special_use = \Archive
}
mailbox "Arkiveret mails" {
special_use = \Archive
}
mailbox "Sendt" {
special_use = \Sent
}
mailbox "Sendte mails" {
special_use = \Sent
}
mailbox "Udkast" {
special_use = \Drafts
}
mailbox "Kladde" {
special_use = \Drafts
}
mailbox "Πρόχειρα" {
special_use = \Drafts
}
mailbox "Απεσταλμένα" {
special_use = \Sent
}
mailbox "Κάδος απορριμάτων" {
special_use = \Trash
}
mailbox "Ανεπιθύμητα" {
special_use = \Junk
}
mailbox "Αρχειοθετημένα" {
special_use = \Archive
}
prefix =
}

View file

@ -0,0 +1,9 @@
#hosts = 1.2.3.4
#dn = cn=admin,dc=example,dc=local
#dnpass = password
#ldap_version = 3
#base = ou=People,dc=example,dc=local
#auth_bind = no
#pass_filter = (&(objectClass=posixAccount)(mail=%u))
#pass_attrs = mail=user,userPassword=password
#default_pass_scheme = SSHA

35
data/conf/mysql/my.cnf Executable file
View file

@ -0,0 +1,35 @@
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
#innodb_file_per_table = TRUE
#innodb_file_format = barracuda
#innodb_large_prefix = TRUE
#sql_mode=IGNORE_SPACE,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
max_allowed_packet = 192M
max-connections = 550
key_buffer_size = 0
read_buffer_size = 192K
sort_buffer_size = 2M
innodb_buffer_pool_size = 24M
read_rnd_buffer_size = 256K
tmp_table_size = 24M
performance_schema = 0
innodb-strict-mode = 0
thread_cache_size = 8
query_cache_type = 0
query_cache_size = 0
max_heap_table_size = 48M
thread_stack = 128K
skip-host-cache
skip-name-resolve
log-warnings = 0
event_scheduler = 1
interactive_timeout = 3610
wait_timeout = 3610
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4

View file

@ -0,0 +1,242 @@
include /etc/nginx/mime.types;
charset utf-8;
override_charset on;
server_tokens off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_ecdh_curve X25519:X448:secp384r1:secp256k1;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=15768000;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header X-Download-Options noopen;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies none;
add_header Referrer-Policy strict-origin;
index index.php index.html;
client_max_body_size 0;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied off;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
location ~ ^/(fonts|js|css|img)/ {
expires max;
add_header Cache-Control public;
}
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
fastcgi_hide_header X-Powered-By;
absolute_redirect off;
root /web;
location / {
try_files $uri $uri/ @strip-ext;
}
location /qhandler {
rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2;
}
location /edit {
rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2;
}
location @strip-ext {
rewrite ^(.*)$ $1.php last;
}
location ~ ^/api/v1/(.*)$ {
try_files $uri $uri/ /json_api.php?query=$1&$args;
}
location ^~ /.well-known/acme-challenge/ {
allow all;
default_type "text/plain";
}
# If behind reverse proxy, forwards the correct IP
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from fc00::/7;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;
location ^~ /principals {
return 301 /SOGo/dav;
}
location ^~ /inc/lib/ {
deny all;
return 403;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass phpfpm:9002;
fastcgi_index index.php;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600;
}
location /rspamd/ {
location /rspamd/auth {
# proxy_pass is not inherited
proxy_pass http://rspamd:11334/auth;
proxy_intercept_errors on;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
error_page 401 /_rspamderror.php;
}
proxy_pass http://rspamd:11334/;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;
}
location ~* ^/Autodiscover/Autodiscover.xml {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass phpfpm:9002;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
try_files /autodiscover.php =404;
}
location ~* ^/Autodiscover/Autodiscover.json {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass phpfpm:9002;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
try_files /autodiscover-json.php =404;
}
location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass phpfpm:9002;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
try_files /autoconfig.php =404;
}
location /sogo-auth-verify {
internal;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Content-Length "";
proxy_pass http://127.0.0.1:65510/sogo-auth;
proxy_pass_request_body off;
}
location ^~ /Microsoft-Server-ActiveSync {
include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf;
include /etc/nginx/conf.d/sogo_eas.active;
proxy_connect_timeout 75;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
proxy_buffer_size 128k;
proxy_buffers 64 512k;
proxy_busy_buffers_size 512k;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
client_body_buffer_size 512k;
client_max_body_size 0;
}
location ^~ /SOGo {
location ~* ^/SOGo/so/.*\.(xml|js|html|xhtml)$ {
include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf;
include /etc/nginx/conf.d/sogo.active;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header x-webobjects-server-protocol HTTP/1.0;
proxy_set_header x-webobjects-remote-host $remote_addr;
proxy_set_header x-webobjects-server-name $server_name;
proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host;
proxy_set_header x-webobjects-server-port $server_port;
proxy_hide_header Content-Type;
add_header Content-Type text/plain;
break;
}
include /etc/nginx/conf.d/includes/sogo_proxy_auth.conf;
include /etc/nginx/conf.d/sogo.active;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header x-webobjects-server-protocol HTTP/1.0;
proxy_set_header x-webobjects-remote-host $remote_addr;
proxy_set_header x-webobjects-server-name $server_name;
proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host;
proxy_set_header x-webobjects-server-port $server_port;
proxy_buffer_size 128k;
proxy_buffers 64 512k;
proxy_busy_buffers_size 512k;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
client_body_buffer_size 128k;
client_max_body_size 0;
break;
}
location ~* /sogo$ {
return 301 $client_req_scheme://$http_host/SOGo;
}
location /SOGo.woa/WebServerResources/ {
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
}
location /.woa/WebServerResources/ {
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
}
location /SOGo/WebServerResources/ {
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
}
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
}
include /etc/nginx/conf.d/site.*.custom;
error_page 502 @awaitingupstream;
location @awaitingupstream {
rewrite ^(.*)$ /_status.502.html break;
}
location ~ ^/cache/(.*)$ {
try_files $uri $uri/ /resource.php?file=$1;
}

View file

@ -0,0 +1,8 @@
auth_request /sogo-auth-verify;
auth_request_set $user $upstream_http_x_user;
auth_request_set $auth $upstream_http_x_auth;
auth_request_set $auth_type $upstream_http_x_auth_type;
proxy_set_header x-webobjects-remote-user "$user";
proxy_set_header Authorization "$auth";
proxy_set_header x-webobjects-auth-type "$auth_type";

View file

@ -0,0 +1,2 @@
listen ${HTTP_PORT};
listen [::]:${HTTP_PORT};

View file

@ -0,0 +1,3 @@
listen ${HTTPS_PORT} ssl;
listen [::]:${HTTPS_PORT} ssl;
http2 on;

View file

@ -0,0 +1 @@
echo "server_name ${MAILCOW_HOSTNAME} autodiscover.* autoconfig.* $(echo ${ADDITIONAL_SERVER_NAMES} | tr ',' ' ');"

View file

@ -0,0 +1,38 @@
echo '
server {
listen 127.0.0.1:65510;
include /etc/nginx/conf.d/listen_plain.active;
include /etc/nginx/conf.d/listen_ssl.active;
ssl_certificate /etc/ssl/mail/cert.pem;
ssl_certificate_key /etc/ssl/mail/key.pem;
include /etc/nginx/conf.d/server_name.active;
include /etc/nginx/conf.d/includes/site-defaults.conf;
}
';
for cert_dir in /etc/ssl/mail/*/ ; do
if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then
continue
fi
# do not create vhost for default-certificate. the cert is already in the default server listen
domains="$(cat ${cert_dir}domains | sed -e 's/^[[:space:]]*//')"
case "${domains}" in
"") continue;;
"${MAILCOW_HOSTNAME}"*) continue;;
esac
echo -n '
server {
include /etc/nginx/conf.d/listen_ssl.active;
ssl_certificate '${cert_dir}'cert.pem;
ssl_certificate_key '${cert_dir}'key.pem;
';
echo -n '
server_name '${domains}';
include /etc/nginx/conf.d/includes/site-defaults.conf;
}
';
done

View file

@ -0,0 +1 @@
proxy_pass http://${IPV4_NETWORK}.248:20000;

View file

@ -0,0 +1,5 @@
if printf "%s\n" "${SKIP_SOGO}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then
echo "return 410;"
else
echo "proxy_pass http://${IPV4_NETWORK}.248:20000/SOGo/Microsoft-Server-ActiveSync;"
fi

View file

@ -0,0 +1,7 @@
opcache.enable=1
opcache.enable_cli=1
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=1

View file

@ -0,0 +1,3 @@
max_execution_time = 3600
max_input_time = 3600
memory_limit = 512M

View file

@ -0,0 +1,3 @@
file_uploads = On
upload_max_filesize = 64M
post_max_size = 64M

View file

@ -0,0 +1,29 @@
[system-worker]
user = www-data
group = www-data
pm = dynamic
pm.max_children = 15
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
listen = [::]:9001
access.log = /proc/self/fd/2
clear_env = no
catch_workers_output = yes
php_admin_value[memory_limit] = 256M
php_admin_value[disable_functions] = show_source, highlight_file, apache_child_terminate, apache_get_modules, apache_note, apache_setenv, virtual, dl, disk_total_space, posix_getpwnam, posix_getpwuid, posix_mkfifo, posix_mknod, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_nice, openlog, syslog, pfsockopen, system, shell_exec, passthru, popen, proc_open, exec, ini_alter, pcntl_exec, proc_close, proc_get_status, proc_terminate, symlink
[web-worker]
user = www-data
group = www-data
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 10
pm.max_spare_servers = 15
listen = [::]:9002
access.log = /proc/self/fd/2
clear_env = no
catch_workers_output = yes
php_admin_value[memory_limit] = 512M
php_admin_value[disable_functions] = show_source, highlight_file, apache_child_terminate, apache_get_modules, apache_note, apache_setenv, virtual, dl, disk_total_space, posix_getpwnam, posix_getpwuid, posix_mkfifo, posix_mknod, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_nice, openlog, syslog, pfsockopen, system, shell_exec, passthru, popen, proc_open, exec, ini_alter, pcntl_exec, proc_close, proc_get_status, proc_terminate, symlink

View file

View file

@ -0,0 +1,20 @@
if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/
/^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
endif
if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/
/^Received: from.* (.*|\n.*)\((.+) (.+)\)\s+by (.+) \(Postcow\) with (.*)/
REPLACE Received: from sieve (sieve $3) by $4 (Postcow) with $5
endif
if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/
/^Received: from.* (.*|\n.*)\((.+) (.+)\)\s+by (.+) \(Postcow\) with (.*)/
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
endif
/^\s*X-Enigmail/ IGNORE
# Not removing Mailer by default, might be signed
#/^\s*X-Mailer/ IGNORE
/^\s*X-Originating-IP/ IGNORE
/^\s*X-Forward/ IGNORE
# Not removing UA by default, might be signed
#/^\s*User-Agent/ IGNORE

View file

@ -0,0 +1,2 @@
/watchdog@localhost$/ watchdog_discard:
/localhost$/ local:

202
data/conf/postfix/main.cf Executable file
View file

@ -0,0 +1,202 @@
# --------------------------------------------------------------------------
# Please create a file "extra.cf" for persistent overrides to main.cf
# --------------------------------------------------------------------------
biff = no
append_dot_mydomain = no
smtpd_tls_cert_file = /etc/ssl/mail/cert.pem
smtpd_tls_key_file = /etc/ssl/mail/key.pem
tls_server_sni_maps = hash:/opt/postfix/conf/sni.map
smtpd_tls_received_header = yes
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_relay_restrictions = permit_mynetworks,
permit_sasl_authenticated,
defer_unauth_destination
smtpd_forbid_bare_newline = yes
# alias maps are auto-generated in postfix.sh on startup
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
relayhost =
mynetworks_style = subnet
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
bounce_queue_lifetime = 1d
broken_sasl_auth_clients = yes
disable_vrfy_command = yes
maximal_backoff_time = 1800s
maximal_queue_lifetime = 5d
delay_warning_time = 4h
message_size_limit = 104857600
milter_default_action = tempfail
milter_protocol = 6
minimal_backoff_time = 300s
plaintext_reject_code = 550
postscreen_access_list = permit_mynetworks,
cidr:/opt/postfix/conf/custom_postscreen_whitelist.cidr,
cidr:/opt/postfix/conf/postscreen_access.cidr,
tcp:127.0.0.1:10027
postscreen_bare_newline_enable = no
postscreen_blacklist_action = drop
postscreen_cache_cleanup_interval = 24h
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
postscreen_dnsbl_action = enforce
postscreen_dnsbl_threshold = 6
postscreen_dnsbl_ttl = 5m
postscreen_greet_action = enforce
postscreen_greet_banner = $smtpd_banner
postscreen_greet_ttl = 2d
postscreen_greet_wait = 3s
postscreen_non_smtp_command_enable = no
postscreen_pipelining_enable = no
proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
$sender_dependent_default_transport_maps,
$smtp_tls_policy_maps,
$local_recipient_maps,
$mydestination,
$virtual_alias_maps,
$virtual_alias_domains,
$virtual_mailbox_maps,
$virtual_mailbox_domains,
$relay_recipient_maps,
$relay_domains,
$canonical_maps,
$sender_canonical_maps,
$sender_bcc_maps,
$recipient_bcc_maps,
$recipient_canonical_maps,
$relocated_maps,
$transport_maps,
$mynetworks,
$smtpd_sender_login_maps,
$smtp_sasl_password_maps
queue_run_delay = 300s
relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
sender_dependent_default_transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_cert_file = /etc/ssl/mail/cert.pem
smtp_tls_key_file = /etc/ssl/mail/key.pem
smtp_tls_loglevel = 1
smtp_dns_support_level = dnssec
smtp_tls_security_level = dane
smtpd_data_restrictions = reject_unauth_pipelining, permit
smtpd_delay_reject = yes
smtpd_error_sleep_time = 10s
smtpd_forbid_bare_newline = yes
smtpd_hard_error_limit = ${stress?1}${stress:5}
smtpd_helo_required = yes
smtpd_proxy_timeout = 600s
smtpd_recipient_restrictions = check_recipient_mx_access proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
permit_sasl_authenticated,
permit_mynetworks,
check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
reject_invalid_helo_hostname,
reject_unauth_destination
smtpd_sasl_auth_enable = yes
smtpd_sasl_authenticated_header = yes
smtpd_sasl_path = inet:dovecot:10001
smtpd_sasl_type = dovecot
smtpd_sender_login_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
permit_mynetworks,
permit_sasl_authenticated,
reject_unlisted_sender,
reject_unknown_sender_domain
smtpd_soft_error_limit = 3
smtpd_tls_auth_only = yes
smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
smtpd_tls_eecdh_grade = auto
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
smtpd_tls_loglevel = 1
# Mandatory protocols and ciphers are used when a connections is enforced to use TLS
# Does _not_ apply to enforced incoming TLS settings per mailbox
smtp_tls_mandatory_protocols = >=TLSv1.2
lmtp_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_ciphers = high
smtp_tls_protocols = >=TLSv1.2
lmtp_tls_protocols = >=TLSv1.2
smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_security_level = may
tls_preempt_cipherlist = yes
tls_ssl_options = NO_COMPRESSION, NO_RENEGOTIATION
virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
virtual_gid_maps = static:5000
virtual_mailbox_base = /var/vmail/
virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
# -- moved to rspamd on 2021-06-01
#recipient_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
#sender_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
recipient_canonical_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
recipient_canonical_classes = envelope_recipient
virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
virtual_minimum_uid = 104
virtual_transport = lmtp:inet:dovecot:24
virtual_uid_maps = static:5000
smtpd_milters = inet:rspamd:9900
non_smtpd_milters = inet:rspamd:9900
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
mydestination = localhost.localdomain, localhost
smtp_address_preference = any
smtp_sender_dependent_authentication = yes
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
smtp_sasl_security_options =
smtp_sasl_mechanism_filter = plain, login
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
mail_name = Postcow
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
# Use custom_transport.pcre for custom transports
transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
pcre:/opt/postfix/conf/local_transport,
proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
smtp_sasl_auth_soft_bounce = no
postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
smtpd_discard_ehlo_keywords = chunking, silent-discard
compatibility_level = 3.7
smtputf8_enable = no
# Define protocols for SMTPS and submission service
submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
# DO NOT EDIT ANYTHING BELOW #
# Overrides #
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
hostkarma.junkemailfilter.com=127.0.0.1*-2
list.dnswl.org=127.0.[0..255].0*-2
list.dnswl.org=127.0.[0..255].1*-4
list.dnswl.org=127.0.[0..255].2*-6
list.dnswl.org=127.0.[0..255].3*-8
ix.dnsbl.manitu.net*2
bl.spamcop.net*2
bl.suomispam.net*2
hostkarma.junkemailfilter.com=127.0.0.2*3
hostkarma.junkemailfilter.com=127.0.0.4*2
hostkarma.junkemailfilter.com=127.0.1.2*1
backscatter.spameatingmonkey.net*2
bl.ipv6.spameatingmonkey.net*2
bl.spameatingmonkey.net*2
b.barracudacentral.org=127.0.0.2*7
bl.mailspike.net=127.0.0.2*5
bl.mailspike.net=127.0.0.[10;11;12]*4
zen.spamhaus.org=127.0.0.[10;11]*8
zen.spamhaus.org=127.0.0.[4..7]*6
zen.spamhaus.org=127.0.0.3*4
zen.spamhaus.org=127.0.0.2*3
# User Overrides
myhostname = hermes.netgrimoire.com

146
data/conf/postfix/master.cf Executable file
View file

@ -0,0 +1,146 @@
# inter-mx with postscreen on 25/tcp
smtp inet n - n - 1 postscreen
10025 inet n - n - 1 postscreen
-o postscreen_upstream_proxy_protocol=haproxy
-o syslog_name=haproxy
smtpd pass - - n - - smtpd
-o smtpd_sasl_auth_enable=no
-o smtpd_sender_restrictions=permit_mynetworks,reject_unlisted_sender,reject_unknown_sender_domain
# smtpd tls-wrapped (smtps) on 465/tcp
# TLS protocol can be modified by setting smtps_smtpd_tls_mandatory_protocols in extra.cf
smtps inet n - n - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_tls_mandatory_protocols=$smtps_smtpd_tls_mandatory_protocols
-o tls_preempt_cipherlist=yes
-o cleanup_service_name=smtp_sender_cleanup
-o syslog_name=postfix/smtps
10465 inet n - n - - smtpd
-o smtpd_upstream_proxy_protocol=haproxy
-o smtpd_tls_wrappermode=yes
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_tls_mandatory_protocols=$smtps_smtpd_tls_mandatory_protocols
-o tls_preempt_cipherlist=yes
-o cleanup_service_name=smtp_sender_cleanup
-o syslog_name=postfix/smtps-haproxy
# smtpd with starttls on 587/tcp
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
submission inet n - n - - smtpd
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_enforce_tls=yes
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
-o tls_preempt_cipherlist=yes
-o cleanup_service_name=smtp_sender_cleanup
-o syslog_name=postfix/submission
10587 inet n - n - - smtpd
-o smtpd_upstream_proxy_protocol=haproxy
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_enforce_tls=yes
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
-o tls_preempt_cipherlist=yes
-o cleanup_service_name=smtp_sender_cleanup
-o syslog_name=postfix/submission-haproxy
# used by SOGo
# smtpd_sender_restrictions should match main.cf, but with check_sasl_access prepended for login-as-mailbox-user function
588 inet n - n - - smtpd
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_tls_auth_only=no
-o smtpd_sender_restrictions=check_sasl_access,regexp:/opt/postfix/conf/allow_mailcow_local.regexp,reject_authenticated_sender_login_mismatch,permit_mynetworks,permit_sasl_authenticated,reject_unlisted_sender,reject_unknown_sender_domain
-o cleanup_service_name=smtp_sender_cleanup
-o syslog_name=postfix/sogo
# used to reinject quarantine mails
590 inet n - n - - smtpd
-o smtpd_helo_restrictions=
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_tls_auth_only=no
-o smtpd_milters=
-o non_smtpd_milters=
-o syslog_name=postfix/quarantine
# used to send bcc mails
591 inet n - n - - smtpd
-o smtpd_helo_restrictions=
-o smtpd_client_restrictions=permit_mynetworks,reject
-o smtpd_tls_auth_only=no
-o smtpd_milters=
-o non_smtpd_milters=
-o syslog_name=postfix/bcc
# enforced smtp connector
smtp_enforced_tls unix - - n - - smtp
-o smtp_tls_security_level=encrypt
-o syslog_name=enforced-tls-smtp
-o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
# smtp connector used, when a transport map matched
# this helps to have different sasl maps than we have with sender dependent transport maps
smtp_via_transport_maps unix - - n - - smtp
-o smtp_sasl_password_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
tlsproxy unix - - n - 0 tlsproxy
dnsblog unix - - n - 0 dnsblog
pickup fifo n - n 60 1 pickup
cleanup unix n - n - 0 cleanup
qmgr fifo n - n 300 1 qmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - n - - smtp
relay unix - - n - - smtp
showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
maildrop unix - n n - - pipe flags=DRhu
user=vmail argv=/usr/bin/maildrop -d ${recipient}
# used to anonymize sender IP
smtp_sender_cleanup unix n - y - 0 cleanup
-o header_checks=$smtp_header_checks
# start whitelist_fwd
127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh
# end whitelist_fwd
# start watchdog-specific
# logs to local7 (hidden)
589 inet n - n - - smtpd
-o smtpd_client_restrictions=permit_mynetworks,reject
-o syslog_name=watchdog
-o syslog_facility=local7
-o smtpd_milters=
-o cleanup_service_name=watchdog_cleanup
-o non_smtpd_milters=
watchdog_cleanup unix n - n - 0 cleanup
-o syslog_name=watchdog
-o syslog_facility=local7
-o queue_service_name=watchdog_qmgr
watchdog_qmgr fifo n - n 300 1 qmgr
-o syslog_facility=local7
-o syslog_name=watchdog
-o rewrite_service_name=watchdog_rewrite
watchdog_rewrite unix - - n - - trivial-rewrite
-o syslog_facility=local7
-o syslog_name=watchdog
-o local_transport=watchdog_discard
watchdog_discard unix - - n - - discard
-o syslog_facility=local7
-o syslog_name=watchdog
# end watchdog-specific

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
/^4(\.\d+\.\d+ TLS is required, but host \S+ refused to start TLS: .+)/
5$1
/^4(\.\d+\.\d+ TLS is required, but was not offered by host .+)/
5$1
/^4.7.5(.*)/
5.7.5$1

View file

@ -0,0 +1,174 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
error_log("ALIASEXP: " . $e . PHP_EOL);
http_response_code(501);
exit;
}
// Init Redis
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
// Read headers
$headers = getallheaders();
// Get rcpt
$rcpt = $headers['Rcpt'];
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
// Parse email address
$parsed_rcpt = parse_email($rcpt);
// Create array of final mailboxes
$rcpt_final_mailboxes = array();
// Skip if not a mailcow handled domain
try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
exit;
}
}
catch (RedisException $e) {
error_log("ALIASEXP: " . $e . PHP_EOL);
http_response_code(504);
exit;
}
// Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
//
// rcpt
// |
// mailbox <-- goto ---> alias1, alias2, mailbox2
// | |
// mailbox3 |
// |
// alias3 ---> mailbox4
//
try {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
$stmt->execute(array(
':rcpt' => $rcpt
));
$gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if (empty($gotos)) {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
$stmt->execute(array(
':rcpt' => '@' . $parsed_rcpt['domain']
));
$gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
}
if (empty($gotos)) {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
$stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
$gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
}
}
$gotos_array = explode(',', $gotos);
$loop_c = 0;
while (count($gotos_array) != 0 && $loop_c <= 20) {
// Loop through all found gotos
foreach ($gotos_array as $index => &$goto) {
error_log("ALIAS EXPANDER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
$stmt->execute(array(':goto' => $goto));
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
if (!empty($username)) {
error_log("ALIAS EXPANDER: http pipe: mailbox found: " . $username . PHP_EOL);
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
if (!in_array($username, $rcpt_final_mailboxes)) {
$rcpt_final_mailboxes[] = $username;
}
}
else {
$parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
$stmt->execute(array(':goto' => $goto));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if ($goto_branch) {
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
error_log("ALIAS EXPANDER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
}
}
}
}
// goto item was processed, unset
unset($gotos_array[$index]);
}
// Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
if (!empty($goto_branch_array)) {
$gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
unset($goto_branch_array);
}
// Reindex array
$gotos_array = array_values($gotos_array);
// Force exit if loop cannot be solved
// Postfix does not allow for alias loops, so this should never happen.
$loop_c++;
error_log("ALIAS EXPANDER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
}
}
catch (PDOException $e) {
error_log("ALIAS EXPANDER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
// Does also return the mailbox name if question == answer (query == mailbox)
if (count($rcpt_final_mailboxes) == 1) {
error_log("ALIASEXP: direct alias " . $rcpt . " expanded to " . $rcpt_final_mailboxes[0] . PHP_EOL);
echo trim($rcpt_final_mailboxes[0]);
}

View file

@ -0,0 +1,88 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
error_log("BCC MAP SQL ERROR: " . $e . PHP_EOL);
http_response_code(501);
exit;
}
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
// Read headers
$headers = getallheaders();
// Get rcpt
$rcpt = $headers['Rcpt'];
// Get from
$from = $headers['From'];
// Remove tags
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
$from = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $from);
try {
if (!empty($rcpt)) {
$stmt = $pdo->prepare("SELECT `bcc_dest` FROM `bcc_maps` WHERE `type` = 'rcpt' AND `local_dest` = :local_dest AND `active` = '1'");
$stmt->execute(array(
':local_dest' => $rcpt
));
$bcc_dest = $stmt->fetch(PDO::FETCH_ASSOC)['bcc_dest'];
if (!empty($bcc_dest) && filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) {
error_log("BCC MAP: returning ". $bcc_dest . " for " . $rcpt . PHP_EOL);
http_response_code(201);
echo trim($bcc_dest);
exit;
}
}
if (!empty($from)) {
$stmt = $pdo->prepare("SELECT `bcc_dest` FROM `bcc_maps` WHERE `type` = 'sender' AND `local_dest` = :local_dest AND `active` = '1'");
$stmt->execute(array(
':local_dest' => $from
));
$bcc_dest = $stmt->fetch(PDO::FETCH_ASSOC)['bcc_dest'];
if (!empty($bcc_dest) && filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) {
error_log("BCC MAP: returning ". $bcc_dest . " for " . $from . PHP_EOL);
http_response_code(201);
echo trim($bcc_dest);
exit;
}
}
}
catch (PDOException $e) {
error_log("BCC MAP SQL ERROR: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}

View file

@ -0,0 +1,113 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
error_log("FOOTER: " . $e . PHP_EOL);
http_response_code(501);
exit;
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
// Read headers
$headers = getallheaders();
// Get Domain
$domain = $headers['Domain'];
// Get Username
$username = $headers['Username'];
// Get From
$from = $headers['From'];
// define empty footer
$empty_footer = json_encode(array(
'html' => '',
'plain' => '',
'skip_replies' => 0,
'vars' => array()
));
error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL);
try {
// try get $target_domain if $domain is an alias_domain
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $domain
));
$alias_domain = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$alias_domain) {
$target_domain = $domain;
} else {
$target_domain = $alias_domain['target_domain'];
}
// get footer associated with the domain
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $target_domain
));
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
// check if the sender is excluded
if (in_array($from, json_decode($footer['mbox_exclude']))){
$footer = false;
}
if (in_array($domain, json_decode($footer['alias_domain_exclude']))){
$footer = false;
}
if (empty($footer)){
echo $empty_footer;
exit;
}
error_log("FOOTER: " . json_encode($footer) . PHP_EOL);
// footer will be applied
// get custom mailbox attributes to insert into the footer
$stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes'];
if (empty($custom_attributes)){
$custom_attributes = (object)array();
}
}
catch (Exception $e) {
error_log("FOOTER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
// return footer
$footer["vars"] = $custom_attributes;
echo json_encode($footer);

View file

@ -0,0 +1,57 @@
<?php
header('Content-Type: text/plain');
ini_set('error_reporting', 0);
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
function in_net($addr, $net) {
$net = explode('/', $net);
if (count($net) > 1) {
$mask = $net[1];
}
$net = inet_pton($net[0]);
$addr = inet_pton($addr);
$length = strlen($net); // 4 for IPv4, 16 for IPv6
if (strlen($net) != strlen($addr)) {
return false;
}
if (!isset($mask)) {
$mask = $length * 8;
}
$addr_bin = '';
$net_bin = '';
for ($i = 0; $i < $length; ++$i) {
$addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT);
$net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT);
}
return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask);
}
if (isset($_GET['host'])) {
try {
foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
if (in_net($_GET['host'], $host)) {
echo '200 PERMIT';
exit;
}
}
echo '200 DUNNO';
}
catch (RedisException $e) {
echo '200 DUNNO';
exit;
}
} else {
try {
echo '240.240.240.240' . PHP_EOL;
foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
echo $host . PHP_EOL;
}
}
catch (RedisException $e) {
echo '240.240.240.240' . PHP_EOL;
exit;
}
}
?>

View file

@ -0,0 +1,2 @@
<html>
</html>

View file

@ -0,0 +1,2 @@
<?php
// PoC

View file

@ -0,0 +1,471 @@
<?php
/*
The match section performs AND operation on different matches: for example, if you have from and rcpt in the same rule,
then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
*/
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Getting headers sent by the client.
ini_set('error_reporting', 0);
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
$stmt = $pdo->query("SELECT '1' FROM `filterconf`");
}
catch (PDOException $e) {
echo 'settings { }';
exit;
}
// Check if db changed and return header
$stmt = $pdo->prepare("SELECT GREATEST(COALESCE(MAX(UNIX_TIMESTAMP(UPDATE_TIME)), 1), COALESCE(MAX(UNIX_TIMESTAMP(CREATE_TIME)), 1)) AS `db_update_time` FROM `information_schema`.`tables`
WHERE (`TABLE_NAME` = 'filterconf' OR `TABLE_NAME` = 'settingsmap' OR `TABLE_NAME` = 'sogo_quick_contact' OR `TABLE_NAME` = 'alias')
AND TABLE_SCHEMA = :dbname;");
$stmt->execute(array(
':dbname' => $database_name
));
$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time'];
if (empty($db_update_time)) {
$db_update_time = 1572048000;
}
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $db_update_time)) {
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304);
exit;
} else {
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200);
}
function parse_email($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a));
}
function normalize_email($email) {
$email = strtolower(str_replace('/', '\/', $email));
$gm = "@gmail.com";
if (substr_compare($email, $gm, -strlen($gm)) == 0) {
$email = explode('@', $email);
$email[0] = str_replace('.', '', $email[0]);
$email = implode('@', $email);
}
$gm_alt = "@googlemail.com";
if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) {
$email = explode('@', $email);
$email[0] = str_replace('.', '', $email[0]);
$email[1] = str_replace('@', '', $gm);
$email = implode('@', $email);
}
if (str_contains($email, "+")) {
$email = explode('@', $email);
$user = explode('+', $email[0]);
$email[0] = $user[0];
$email = implode('@', $email);
}
return $email;
}
function wl_by_sogo() {
global $pdo;
$rcpt = array();
$stmt = $pdo->query("SELECT DISTINCT(`sogo_folder_info`.`c_path2`) AS `user`, GROUP_CONCAT(`sogo_quick_contact`.`c_mail`) AS `contacts` FROM `sogo_folder_info`
INNER JOIN `sogo_quick_contact` ON `sogo_quick_contact`.`c_folder_id` = `sogo_folder_info`.`c_folder_id`
GROUP BY `c_path2`");
$sogo_contacts = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($sogo_contacts)) {
foreach (explode(',', $row['contacts']) as $contact) {
if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) {
continue;
}
// Explicit from, no mime_from, no regex - envelope must match
// mailcow white and blacklists also cover mime_from
$rcpt[$row['user']][] = normalize_email($contact);
}
}
return $rcpt;
}
function ucl_rcpts($object, $type) {
global $pdo;
$rcpt = array();
if ($type == 'mailbox') {
// Standard aliases
$stmt = $pdo->prepare("SELECT `address` FROM `alias`
WHERE `goto` = :object_goto
AND `address` NOT LIKE '@%'");
$stmt->execute(array(
':object_goto' => $object
));
$standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($standard_aliases)) {
$local = parse_email($row['address'])['local'];
$domain = parse_email($row['address'])['domain'];
if (!empty($local) && !empty($domain)) {
$rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
}
$rcpt[] = str_replace('/', '\/', $row['address']);
}
// Aliases by alias domains
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object");
$stmt->execute(array(
':object' => $object
));
$by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($by_domain_aliases);
while ($row = array_shift($by_domain_aliases)) {
if (!empty($row['alias'])) {
$local = parse_email($row['alias'])['local'];
$domain = parse_email($row['alias'])['domain'];
if (!empty($local) && !empty($domain)) {
$rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i';
}
$rcpt[] = str_replace('/', '\/', $row['alias']);
}
}
}
elseif ($type == 'domain') {
// Domain self
$rcpt[] = '/.*@' . $object . '/i';
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
WHERE `target_domain` = :object");
$stmt->execute(array(':object' => $object));
$alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
array_filter($alias_domains);
while ($row = array_shift($alias_domains)) {
$rcpt[] = '/.*@' . $row['alias_domain'] . '/i';
}
}
return $rcpt;
}
?>
settings {
watchdog {
priority = 10;
rcpt_mime = "/null@localhost/i";
from_mime = "/watchdog@localhost/i";
apply "default" {
symbols_disabled = ["HISTORY_SAVE", "ARC", "ARC_SIGNED", "DKIM", "DKIM_SIGNED", "CLAM_VIRUS"];
want_spam = yes;
actions {
reject = 9999.0;
greylist = 9998.0;
"add header" = 9997.0;
}
}
}
<?php
/*
// Start custom scores for users
*/
$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
?>
score_<?=$username_sane;?> {
priority = 4;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
AND `object`= :object");
$stmt->execute(array(':object' => $row['object']));
$spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
?>
apply "default" {
actions {
reject = <?=$spamscore['highspamlevel'][0];?>;
greylist = <?=$spamscore['lowspamlevel'][0] - 1;?>;
"add header" = <?=$spamscore['lowspamlevel'][0];?>;
}
}
}
<?php
}
/*
// Start SOGo contacts whitelist
// Priority 4, lower than a domain whitelist (5) and lower than a mailbox whitelist (6)
*/
foreach (wl_by_sogo() as $user => $contacts) {
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $user);
?>
whitelist_sogo_<?=$username_sane;?> {
<?php
foreach ($contacts as $contact) {
?>
from = <?=json_encode($contact, JSON_UNESCAPED_SLASHES);?>;
<?php
}
?>
priority = 4;
<?php
foreach (ucl_rcpts($user, 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
?>
apply "default" {
SOGO_CONTACT = -99.0;
}
symbols [
"SOGO_CONTACT"
]
}
<?php
}
/*
// Start whitelist
*/
$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'whitelist_from'");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
?>
whitelist_<?=$username_sane;?> {
<?php
$list_items = array();
$stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
WHERE `object`= :object
AND `option` = 'whitelist_from'");
$stmt->execute(array(':object' => $row['object']));
$list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($list_items as $item) {
?>
from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
<?php
}
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
?>
priority = 5;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
else {
?>
priority = 6;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
?>
apply "default" {
MAILCOW_WHITE = -999.0;
}
symbols [
"MAILCOW_WHITE"
]
}
whitelist_mime_<?=$username_sane;?> {
<?php
foreach ($list_items as $item) {
?>
from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
<?php
}
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
?>
priority = 5;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
else {
?>
priority = 6;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
?>
apply "default" {
MAILCOW_WHITE = -999.0;
}
symbols [
"MAILCOW_WHITE"
]
}
<?php
}
/*
// Start blacklist
*/
$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'blacklist_from'");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
?>
blacklist_<?=$username_sane;?> {
<?php
$list_items = array();
$stmt = $pdo->prepare("SELECT `value` FROM `filterconf`
WHERE `object`= :object
AND `option` = 'blacklist_from'");
$stmt->execute(array(':object' => $row['object']));
$list_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($list_items as $item) {
?>
from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
<?php
}
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
?>
priority = 5;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
else {
?>
priority = 6;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
?>
apply "default" {
MAILCOW_BLACK = 999.0;
}
symbols [
"MAILCOW_BLACK"
]
}
blacklist_header_<?=$username_sane;?> {
<?php
foreach ($list_items as $item) {
?>
from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i";
<?php
}
if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
?>
priority = 5;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
else {
?>
priority = 6;
<?php
foreach (ucl_rcpts($row['object'], strpos($row['object'], '@') === FALSE ? 'domain' : 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
}
?>
apply "default" {
MAILCOW_BLACK = 999.0;
}
symbols [
"MAILCOW_BLACK"
]
}
<?php
}
/*
// Start traps
*/
?>
ham_trap {
<?php
foreach (ucl_rcpts('ham@localhost', 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
?>
priority = 9;
apply "default" {
symbols_enabled = ["HISTORY_SAVE"];
}
symbols [
"HAM_TRAP"
]
}
spam_trap {
<?php
foreach (ucl_rcpts('spam@localhost', 'mailbox') as $rcpt) {
?>
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
?>
priority = 9;
apply "default" {
symbols_enabled = ["HISTORY_SAVE"];
}
symbols [
"SPAM_TRAP"
]
}
<?php
// Start additional content
$stmt = $pdo->query("SELECT `id`, `content` FROM `settingsmap` WHERE `active` = '1'");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['id']);
?>
additional_settings_<?=intval($row['id']);?> {
<?php
$content = preg_split('/\r\n|\r|\n/', $row['content']);
foreach ($content as $line) {
echo ' ' . $line . PHP_EOL;
}
?>
}
<?php
}
?>
}

View file

@ -0,0 +1,6 @@
<?php
require_once('../../../web/inc/vars.inc.php');
if (file_exists('../../../web/inc/vars.local.inc.php')) {
include_once('../../../web/inc/vars.local.inc.php');
}
?>

View file

@ -0,0 +1,16 @@
local custom_keywords = {}
custom_keywords.mailcow = function(task)
local rspamd_logger = require "rspamd_logger"
local dyn_rl_symbol = task:get_symbol("DYN_RL")
if dyn_rl_symbol then
local rl_value = dyn_rl_symbol[1].options[1]
local rl_object = dyn_rl_symbol[1].options[2]
if rl_value and rl_object then
rspamd_logger.infox(rspamd_config, "DYN_RL symbol has value %s for object %s, returning %s...", rl_value, rl_object, "rs_dynrl_" .. rl_object)
return "rs_dynrl_" .. rl_object, rl_value
end
end
end
return custom_keywords

View file

@ -0,0 +1,701 @@
rspamd_config.MAILCOW_AUTH = {
callback = function(task)
local uname = task:get_user()
if uname then
return 1
end
end
}
local monitoring_hosts = rspamd_config:add_map{
url = "/etc/rspamd/custom/monitoring_nolog.map",
description = "Monitoring hosts",
type = "regexp"
}
rspamd_config:register_symbol({
name = 'SMTP_ACCESS',
type = 'postfilter',
callback = function(task)
local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger"
local rspamd_ip = require 'rspamd_ip'
local uname = task:get_user()
local limited_access = task:get_symbol("SMTP_LIMITED_ACCESS")
if not uname then
return false
end
if not limited_access then
return false
end
local hash_key = 'SMTP_ALLOW_NETS_' .. uname
local redis_params = rspamd_parse_redis_server('smtp_access')
local ip = task:get_from_ip()
if ip == nil or not ip:is_valid() then
return false
end
local from_ip_string = tostring(ip)
smtp_access_table = {from_ip_string}
local maxbits = 128
local minbits = 32
if ip:get_version() == 4 then
maxbits = 32
minbits = 8
end
for i=maxbits,minbits,-1 do
local nip = ip:apply_mask(i):to_string() .. "/" .. i
table.insert(smtp_access_table, nip)
end
local function smtp_access_cb(err, data)
if err then
rspamd_logger.infox(rspamd_config, "smtp_access query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
return false
else
rspamd_logger.infox(rspamd_config, "checking ip %s for smtp_access in %s", from_ip_string, hash_key)
for k,v in pairs(data) do
if (v and v ~= userdata and v == '1') then
rspamd_logger.infox(rspamd_config, "found ip in smtp_access map")
task:insert_result(true, 'SMTP_ACCESS', 0.0, from_ip_string)
return true
end
end
rspamd_logger.infox(rspamd_config, "couldnt find ip in smtp_access map")
task:insert_result(true, 'SMTP_ACCESS', 999.0, from_ip_string)
return true
end
end
table.insert(smtp_access_table, 1, hash_key)
local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params
hash_key, -- hash key
false, -- is write
smtp_access_cb, --callback
'HMGET', -- command
smtp_access_table -- arguments
)
if not redis_ret_user then
rspamd_logger.infox(rspamd_config, "cannot check smtp_access redis map")
end
end,
priority = 10
})
rspamd_config:register_symbol({
name = 'POSTMASTER_HANDLER',
type = 'prefilter',
callback = function(task)
local rcpts = task:get_recipients('smtp')
local rspamd_logger = require "rspamd_logger"
local lua_util = require "lua_util"
local from = task:get_from(1)
-- not applying to mails with more than one rcpt to avoid bypassing filters by addressing postmaster
if rcpts and #rcpts == 1 then
for _,rcpt in ipairs(rcpts) do
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
return
end
end
end
end
if from then
for _,fr in ipairs(from) do
local fr_split = rspamd_str_split(fr['addr'], '@')
if #fr_split == 2 then
if fr_split[1] == 'postmaster' and task:get_user() then
-- no whitelist, keep signatures
task:insert_result(true, 'POSTMASTER_FROM', -2500.0)
return
end
end
end
end
end,
priority = 10
})
rspamd_config:register_symbol({
name = 'KEEP_SPAM',
type = 'prefilter',
callback = function(task)
local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger"
local rspamd_ip = require 'rspamd_ip'
local uname = task:get_user()
if uname then
return false
end
local redis_params = rspamd_parse_redis_server('keep_spam')
local ip = task:get_from_ip()
if ip == nil or not ip:is_valid() then
return false
end
local from_ip_string = tostring(ip)
ip_check_table = {from_ip_string}
local maxbits = 128
local minbits = 32
if ip:get_version() == 4 then
maxbits = 32
minbits = 8
end
for i=maxbits,minbits,-1 do
local nip = ip:apply_mask(i):to_string() .. "/" .. i
table.insert(ip_check_table, nip)
end
local function keep_spam_cb(err, data)
if err then
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
return false
else
for k,v in pairs(data) do
if (v and v ~= userdata and v == '1') then
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
task:set_pre_result('accept', 'ip matched with forward hosts')
end
end
end
end
table.insert(ip_check_table, 1, 'KEEP_SPAM')
local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params
'KEEP_SPAM', -- hash key
false, -- is write
keep_spam_cb, --callback
'HMGET', -- command
ip_check_table -- arguments
)
if not redis_ret_user then
rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map")
end
end,
priority = 19
})
rspamd_config:register_symbol({
name = 'TLS_HEADER',
type = 'postfilter',
callback = function(task)
local rspamd_logger = require "rspamd_logger"
local tls_tag = task:get_request_header('TLS-Version')
if type(tls_tag) == 'nil' then
task:set_milter_reply({
add_headers = {['X-Last-TLS-Session-Version'] = 'None'}
})
else
task:set_milter_reply({
add_headers = {['X-Last-TLS-Session-Version'] = tostring(tls_tag)}
})
end
end,
priority = 12
})
rspamd_config:register_symbol({
name = 'TAG_MOO',
type = 'postfilter',
callback = function(task)
local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger"
local redis_params = rspamd_parse_redis_server('taghandler')
local rspamd_http = require "rspamd_http"
local rcpts = task:get_recipients('smtp')
local lua_util = require "lua_util"
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false)
if moo_tag_header then
task:set_milter_reply({
remove_headers = {['X-Moo-Tag'] = 0},
})
end
return true
end
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
local tag = tagged_rcpt[1].options[1]
rspamd_logger.infox("found tag: %s", tag)
local action = task:get_metric_action('default')
rspamd_logger.infox("metric action now: %s", action)
if action ~= 'no action' and action ~= 'greylist' then
rspamd_logger.infox("skipping tag handler for action: %s", action)
remove_moo_tag()
return true
end
local function http_callback(err_message, code, body, headers)
if body ~= nil and body ~= "" then
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
local function tag_callback_subject(err, data)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
local function tag_callback_subfolder(err, data)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag()
else
rspamd_logger.infox("Add X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local redis_ret_subfolder = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subfolder, --callback
'HGET', -- command
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
)
if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
else
rspamd_logger.infox("user wants subject modified for tagged mail")
local sbj = task:get_header('Subject')
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end
end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
end
end
if rcpts and #rcpts == 1 then
for _,rcpt in ipairs(rcpts) do
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
remove_moo_tag()
else
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt['addr']},
})
end
end
end
end
else
remove_moo_tag()
end
end,
priority = 19
})
rspamd_config:register_symbol({
name = 'BCC',
type = 'postfilter',
callback = function(task)
local util = require("rspamd_util")
local rspamd_http = require "rspamd_http"
local rspamd_logger = require "rspamd_logger"
local from_table = {}
local rcpt_table = {}
if task:has_symbol('ENCRYPTED_CHAT') then
return -- stop
end
local send_mail = function(task, bcc_dest)
local lua_smtp = require "lua_smtp"
local function sendmail_cb(ret, err)
if not ret then
rspamd_logger.errx(task, 'BCC SMTP ERROR: %s', err)
else
rspamd_logger.infox(rspamd_config, "BCC SMTP SUCCESS TO %s", bcc_dest)
end
end
if not bcc_dest then
return -- stop
end
-- dot stuff content before sending
local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail
lua_smtp.sendmail({
task = task,
host = os.getenv("IPV4_NETWORK") .. '.253',
port = 591,
from = task:get_from(stp)[1].addr,
recipients = bcc_dest,
helo = 'bcc',
timeout = 20,
}, email_content, sendmail_cb)
end
-- determine from
local from = task:get_from('smtp')
if from then
for _, a in ipairs(from) do
table.insert(from_table, a['addr']) -- add this rcpt to table
table.insert(from_table, '@' .. a['domain']) -- add this rcpts domain to table
end
else
return -- stop
end
-- determine rcpts
local rcpts = task:get_recipients('smtp')
if rcpts then
for _, a in ipairs(rcpts) do
table.insert(rcpt_table, a['addr']) -- add this rcpt to table
table.insert(rcpt_table, '@' .. a['domain']) -- add this rcpts domain to table
end
else
return -- stop
end
local action = task:get_metric_action('default')
rspamd_logger.infox("metric action now: %s", action)
local function rcpt_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
send_mail(task, body)
end
end
end
local function from_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
send_mail(task, body)
end
end
end
if rcpt_table then
for _,e in ipairs(rcpt_table) do
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
rspamd_http.request({
task=task,
url='http://nginx:8081/bcc.php',
body='',
callback=rcpt_callback,
headers={Rcpt=e}
})
end
end
if from_table then
for _,e in ipairs(from_table) do
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
rspamd_http.request({
task=task,
url='http://nginx:8081/bcc.php',
body='',
callback=from_callback,
headers={From=e}
})
end
end
return true
end,
priority = 20
})
rspamd_config:register_symbol({
name = 'DYN_RL_CHECK',
type = 'prefilter',
callback = function(task)
local util = require("rspamd_util")
local redis_params = rspamd_parse_redis_server('dyn_rl')
local rspamd_logger = require "rspamd_logger"
local envfrom = task:get_from(1)
local uname = task:get_user()
if not envfrom or not uname then
return false
end
local uname = uname:lower()
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
local function redis_cb_user(err, data)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for user %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying dynamic ratelimit for domain...", uname, data, err)
local function redis_key_cb_domain(err, data)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for domain %s returned invalid or empty data (\"%s\") or error (\"%s\")", env_from_domain, data, err)
else
rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for domain %s with value %s", env_from_domain, data)
task:insert_result('DYN_RL', 0.0, data, env_from_domain)
end
end
local redis_ret_domain = rspamd_redis_make_request(task,
redis_params, -- connect params
env_from_domain, -- hash key
false, -- is write
redis_key_cb_domain, --callback
'HGET', -- command
{'RL_VALUE', env_from_domain} -- arguments
)
if not redis_ret_domain then
rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for domain")
end
else
rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for user %s with value %s", uname, data)
task:insert_result('DYN_RL', 0.0, data, uname)
end
end
local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params
uname, -- hash key
false, -- is write
redis_cb_user, --callback
'HGET', -- command
{'RL_VALUE', uname} -- arguments
)
if not redis_ret_user then
rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for user")
end
return true
end,
flags = 'empty',
priority = 20
})
rspamd_config:register_symbol({
name = 'NO_LOG_STAT',
type = 'postfilter',
callback = function(task)
local from = task:get_header('From')
if from and (monitoring_hosts:get_key(from) or from == "watchdog@localhost") then
task:set_flag('no_log')
task:set_flag('no_stat')
end
end
})
rspamd_config:register_symbol({
name = 'MOO_FOOTER',
type = 'prefilter',
callback = function(task)
local cjson = require "cjson"
local lua_mime = require "lua_mime"
local lua_util = require "lua_util"
local rspamd_logger = require "rspamd_logger"
local rspamd_http = require "rspamd_http"
local envfrom = task:get_from(1)
local uname = task:get_user()
if not envfrom or not uname then
return false
end
local uname = uname:lower()
local env_from_domain = envfrom[1].domain:lower()
local env_from_addr = envfrom[1].addr:lower()
-- determine newline type
local function newline(task)
local t = task:get_newlines_type()
if t == 'cr' then
return '\r'
elseif t == 'lf' then
return '\n'
end
return '\r\n'
end
-- retrieve footer
local function footer_cb(err_message, code, data, headers)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
else
-- parse json string
local footer = cjson.decode(data)
if not footer then
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
else
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
if footer.skip_replies ~= 0 then
in_reply_to = task:get_header_raw('in-reply-to')
if in_reply_to then
rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
return
end
end
local envfrom_mime = task:get_from(2)
local from_name = ""
if envfrom_mime and envfrom_mime[1].name then
from_name = envfrom_mime[1].name
elseif envfrom and envfrom[1].name then
from_name = envfrom[1].name
end
-- default replacements
local replacements = {
auth_user = uname,
from_user = envfrom[1].user,
from_name = from_name,
from_addr = envfrom[1].addr,
from_domain = envfrom[1].domain:lower()
}
-- add custom mailbox attributes
if footer.vars and type(footer.vars) == "string" then
local footer_vars = cjson.decode(footer.vars)
if type(footer_vars) == "table" then
for key, value in pairs(footer_vars) do
replacements[key] = value
end
end
end
if footer.html and footer.html ~= "" then
footer.html = lua_util.jinja_template(footer.html, replacements, true)
end
if footer.plain and footer.plain ~= "" then
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
end
-- add footer
local out = {}
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
local seen_cte
local newline_s = newline(task)
local function rewrite_ct_cb(name, hdr)
if rewrite.need_rewrite_ct then
if name:lower() == 'content-type' then
local nct = string.format('%s: %s/%s; charset=utf-8',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
out[#out + 1] = nct
-- update Content-Type header
task:set_milter_reply({
remove_headers = {['Content-Type'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
})
return
elseif name:lower() == 'content-transfer-encoding' then
out[#out + 1] = string.format('%s: %s',
'Content-Transfer-Encoding', 'quoted-printable')
-- update Content-Transfer-Encoding header
task:set_milter_reply({
remove_headers = {['Content-Transfer-Encoding'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
})
seen_cte = true
return
end
end
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
end
task:headers_foreach(rewrite_ct_cb, {full = true})
if not seen_cte and rewrite.need_rewrite_ct then
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
end
-- End of headers
out[#out + 1] = newline_s
if rewrite.out then
for _,o in ipairs(rewrite.out) do
out[#out + 1] = o
end
else
out[#out + 1] = task:get_rawbody()
end
local out_parts = {}
for _,o in ipairs(out) do
if type(o) ~= 'table' then
out_parts[#out_parts + 1] = o
out_parts[#out_parts + 1] = newline_s
else
local removePrefix = "--\x0D\x0AContent-Type"
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
end
out_parts[#out_parts + 1] = o[1]
if o[2] then
out_parts[#out_parts + 1] = newline_s
end
end
end
task:set_message(out_parts)
else
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
end
end
end
end
-- fetch footer
rspamd_http.request({
task=task,
url='http://nginx:8081/footer.php',
body='',
callback=footer_cb,
headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
})
return true
end,
priority = 1
})

View file

@ -0,0 +1,260 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
error_log("QUARANTINE: " . $e . PHP_EOL);
http_response_code(501);
exit;
}
// Init Redis
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
// Functions
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
$raw_data_content = file_get_contents('php://input');
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
$headers = getallheaders();
$qid = $headers['X-Rspamd-Qid'];
$fuzzy = $headers['X-Rspamd-Fuzzy'];
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$score = $headers['X-Rspamd-Score'];
$rcpts = $headers['X-Rspamd-Rcpt'];
$user = $headers['X-Rspamd-User'];
$ip = $headers['X-Rspamd-Ip'];
$action = $headers['X-Rspamd-Action'];
$sender = $headers['X-Rspamd-From'];
$symbols = $headers['X-Rspamd-Symbols'];
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
if (empty($sender)) {
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
$sender = 'empty-env-from@localhost';
}
if ($fuzzy == 'unknown') {
$fuzzy = '[]';
}
try {
$max_size = (int)$redis->Get('Q_MAX_SIZE');
if (($max_size * 1048576) < $raw_size) {
error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL);
http_response_code(505);
exit;
}
if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) {
$exclude_domains = json_decode($exclude_domains, true);
}
$retention_size = (int)$redis->Get('Q_RETENTION_SIZE');
}
catch (RedisException $e) {
error_log("QUARANTINE: " . $e . PHP_EOL);
http_response_code(504);
exit;
}
$rcpt_final_mailboxes = array();
// Loop through all rcpts
foreach (json_decode($rcpts, true) as $rcpt) {
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
// Break rcpt into local part and domain part
$parsed_rcpt = parse_email($rcpt);
// Skip if not a mailcow handled domain
try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue;
}
}
catch (RedisException $e) {
error_log("QUARANTINE: " . $e . PHP_EOL);
http_response_code(504);
exit;
}
// Skip if domain is excluded
if (in_array($parsed_rcpt['domain'], $exclude_domains)) {
error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL);
continue;
}
// Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
//
// rcpt
// |
// mailbox <-- goto ---> alias1, alias2, mailbox2
// | |
// mailbox3 |
// |
// alias3 ---> mailbox4
//
try {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
$stmt->execute(array(
':rcpt' => $rcpt
));
$gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if (empty($gotos)) {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
$stmt->execute(array(
':rcpt' => '@' . $parsed_rcpt['domain']
));
$gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
}
if (empty($gotos)) {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
$stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
$gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
}
}
$gotos_array = explode(',', $gotos);
$loop_c = 0;
while (count($gotos_array) != 0 && $loop_c <= 20) {
// Loop through all found gotos
foreach ($gotos_array as $index => &$goto) {
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
$stmt->execute(array(':goto' => $goto));
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
if (!empty($username)) {
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
if (!in_array($username, $rcpt_final_mailboxes)) {
$rcpt_final_mailboxes[] = $username;
}
}
else {
$parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
$stmt->execute(array(':goto' => $goto));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
}
}
}
}
// goto item was processed, unset
unset($gotos_array[$index]);
}
// Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
if (!empty($goto_branch_array)) {
$gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
unset($goto_branch_array);
}
// Reindex array
$gotos_array = array_values($gotos_array);
// Force exit if loop cannot be solved
// Postfix does not allow for alias loops, so this should never happen.
$loop_c++;
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
}
}
catch (PDOException $e) {
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
}
foreach ($rcpt_final_mailboxes as $rcpt_final) {
error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt_final . PHP_EOL);
try {
$stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`)
VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes)");
$stmt->execute(array(
':qid' => $qid,
':subject' => $subject,
':score' => $score,
':sender' => $sender,
':rcpt' => $rcpt_final,
':symbols' => $symbols,
':user' => $user,
':ip' => $ip,
':msg' => $raw_data,
':action' => $action,
':fuzzy_hashes' => $fuzzy
));
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
SELECT `id`
FROM (
SELECT `id`
FROM `quarantine`
WHERE `rcpt` = :rcpt2
ORDER BY id DESC
LIMIT :retention_size
) x
);');
$stmt->execute(array(
':rcpt' => $rcpt_final,
':rcpt2' => $rcpt_final,
':retention_size' => $retention_size
));
}
catch (PDOException $e) {
error_log("QUARANTINE: " . $e->getMessage() . PHP_EOL);
http_response_code(503);
exit;
}
}

View file

@ -0,0 +1,48 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$redis->connect('redis-mailcow', 6379);
}
}
catch (Exception $e) {
exit;
}
$raw_data_content = file_get_contents('php://input');
$raw_data_decoded = json_decode($raw_data_content, true);
$data['time'] = time();
$data['rcpt'] = implode(', ', $raw_data_decoded['rcpt']);
$data['from'] = $raw_data_decoded['from'];
$data['user'] = $raw_data_decoded['user'];
$symbol_rl_key = array_search('RATELIMITED', array_column($raw_data_decoded['symbols'], 'name'));
$data['rl_info'] = implode($raw_data_decoded['symbols'][$symbol_rl_key]['options']);
preg_match('/(.+)\((.+)\)/i', $data['rl_info'], $rl_matches);
if (!empty($rl_matches[1]) && !empty($rl_matches[2])) {
$data['rl_name'] = $rl_matches[1];
$data['rl_hash'] = $rl_matches[2];
}
else {
$data['rl_name'] = 'err';
$data['rl_hash'] = 'err';
}
$data['qid'] = $raw_data_decoded['qid'];
$data['ip'] = $raw_data_decoded['ip'];
$data['message_id'] = $raw_data_decoded['message_id'];
$data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']);
$data['header_from'] = implode(', ', $raw_data_decoded['header_from']);
$redis->lpush('RL_LOG', json_encode($data));
exit;

View file

@ -0,0 +1,275 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
error_log("NOTIFY: " . $e . PHP_EOL);
http_response_code(501);
exit;
}
// Init Redis
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
// Functions
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
$headers = getallheaders();
$json_body = json_decode(file_get_contents('php://input'));
$qid = $headers['X-Rspamd-Qid'];
$rcpts = $headers['X-Rspamd-Rcpt'];
$sender = $headers['X-Rspamd-From'];
$ip = $headers['X-Rspamd-Ip'];
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$messageid= $json_body->message_id;
$priority = 0;
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
if (is_array($symbols_array)) {
foreach ($symbols_array as $symbol) {
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
$priority = 1;
break;
}
}
}
$sender_address = $json_body->header_from[0];
$sender_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
$sender_address = $matches['address'];
$sender_name = trim($matches['name'], '"\' ');
}
$to_address = $json_body->header_to[0];
$to_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
$to_address = $matches['address'];
$to_name = trim($matches['name'], '"\' ');
}
$rcpt_final_mailboxes = array();
// Loop through all rcpts
foreach (json_decode($rcpts, true) as $rcpt) {
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
// Break rcpt into local part and domain part
$parsed_rcpt = parse_email($rcpt);
// Skip if not a mailcow handled domain
try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue;
}
}
catch (RedisException $e) {
error_log("NOTIFY: " . $e . PHP_EOL);
http_response_code(504);
exit;
}
// Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases
//
// rcpt
// |
// mailbox <-- goto ---> alias1, alias2, mailbox2
// | |
// mailbox3 |
// |
// alias3 ---> mailbox4
//
try {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
$stmt->execute(array(
':rcpt' => $rcpt
));
$gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if (empty($gotos)) {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'");
$stmt->execute(array(
':rcpt' => '@' . $parsed_rcpt['domain']
));
$gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
}
if (empty($gotos)) {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'");
$stmt->execute(array(':rcpt' => $parsed_rcpt['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
$gotos = $parsed_rcpt['local'] . '@' . $goto_branch;
}
}
$gotos_array = explode(',', $gotos);
$loop_c = 0;
while (count($gotos_array) != 0 && $loop_c <= 20) {
// Loop through all found gotos
foreach ($gotos_array as $index => &$goto) {
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
$stmt->execute(array(':goto' => $goto));
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
if (!empty($username)) {
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
if (!in_array($username, $rcpt_final_mailboxes)) {
$rcpt_final_mailboxes[] = $username;
}
}
else {
$parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
$stmt->execute(array(':goto' => $goto));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
}
}
}
}
// goto item was processed, unset
unset($gotos_array[$index]);
}
// Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array
if (!empty($goto_branch_array)) {
$gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array));
unset($goto_branch_array);
}
// Reindex array
$gotos_array = array_values($gotos_array);
// Force exit if loop cannot be solved
// Postfix does not allow for alias loops, so this should never happen.
$loop_c++;
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
}
}
catch (PDOException $e) {
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
}
foreach ($rcpt_final_mailboxes as $rcpt_final) {
error_log("NOTIFY: pushover pipe: processing pushover message for rcpt " . $rcpt_final . PHP_EOL);
$stmt = $pdo->prepare("SELECT * FROM `pushover`
WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(
':username' => $rcpt_final
));
$api_data = $stmt->fetch(PDO::FETCH_ASSOC);
if (isset($api_data['key']) && isset($api_data['token'])) {
$title = (!empty($api_data['title'])) ? $api_data['title'] : 'Mail';
$text = (!empty($api_data['text'])) ? $api_data['text'] : 'You\'ve got mail 📧';
$attributes = json_decode($api_data['attributes'], true);
$senders = explode(',', $api_data['senders']);
$senders = array_filter($senders);
$senders_regex = $api_data['senders_regex'];
$sender_validated = false;
if (empty($senders) && empty($senders_regex)) {
$sender_validated = true;
}
else {
if (!empty($senders)) {
if (in_array($sender, $senders)) {
$sender_validated = true;
}
}
if (!empty($senders_regex) && $sender_validated !== true) {
if (preg_match($senders_regex, $sender)) {
$sender_validated = true;
}
}
}
if ($sender_validated === false) {
error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender);
continue;
}
if ($attributes['only_x_prio'] == "1" && $priority == 0) {
error_log("NOTIFY: pushover pipe: mail has no X-Priority: 1 header, skipping");
continue;
}
$post_fields = array(
"token" => $api_data['token'],
"user" => $api_data['key'],
"title" => sprintf("%s", str_replace(
array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}'),
array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid), $title)
),
"priority" => $priority,
"message" => sprintf("%s", str_replace(
array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}', '\n'),
array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid, PHP_EOL), $text)
),
"sound" => $attributes['sound'] ?? "pushover"
);
if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) {
$post_fields['expire'] = 600;
$post_fields['retry'] = 120;
$post_fields['priority'] = 2;
}
curl_setopt_array($ch = curl_init(), array(
CURLOPT_URL => "https://api.pushover.net/1/messages.json",
CURLOPT_POSTFIELDS => $post_fields,
CURLOPT_SAFE_UPLOAD => true,
CURLOPT_RETURNTRANSFER => true,
));
$result = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("NOTIFY: result: " . $httpcode . PHP_EOL);
}
}

View file

@ -0,0 +1,6 @@
<?php
require_once('../../../web/inc/vars.inc.php');
if (file_exists('../../../web/inc/vars.local.inc.php')) {
include_once('../../../web/inc/vars.local.inc.php');
}
?>

View file

@ -0,0 +1 @@
This is where you should copy any rspamd custom module

View file

@ -0,0 +1 @@
# rspamd.conf.local

View file

@ -0,0 +1,2 @@
# rspamd.conf.override

BIN
data/conf/sogo/custom-favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

7
data/conf/sogo/custom-sogo.js Executable file
View file

@ -0,0 +1,7 @@
// Custom SOGo JS
// Change the visible font-size in the editor, this does not change the font of a html message by default
CKEDITOR.addCss("body {font-size: 16px !important}");
// Enable scayt by default
//CKEDITOR.config.scayt_autoStartup = true;

98
data/conf/sogo/sogo.conf Executable file
View file

@ -0,0 +1,98 @@
{
SOGoCalendarDefaultRoles = (
PublicViewer,
ConfidentialDAndTViewer,
PrivateDAndTViewer
);
WOWorkersCount = "20";
SOGoACLsSendEMailNotifications = YES;
SOGoAppointmentSendEMailNotifications = YES;
SOGoDraftsFolderName = "Drafts";
SOGoJunkFolderName= "Junk";
SOGoMailDomain = "sogo.local";
SOGoEnableEMailAlarms = YES;
SOGoMailHideInlineAttachments = YES;
SOGoFoldersSendEMailNotifications = YES;
SOGoForwardEnabled = YES;
// Option to set Users as admin to globally manage calendar permissions etc. Disabled by default
// SOGoSuperUsernames = ("moo@example.com");
SOGoUIAdditionalJSFiles = (
js/theme.js,
js/custom-sogo.js
);
SOGoEnablePublicAccess = YES;
// Multi-domain setup
// Domains are isolated, you can define visibility options here.
// Example:
// SOGoDomainsVisibility = (
// (domain1.tld, domain5.tld),
// (domain3.tld, domain2.tld)
// );
// self-signed is not trusted anymore
WOPort = "0.0.0.0:20000";
SOGoMemcachedHost = "memcached";
SOGoLanguage = English;
SOGoMailAuxiliaryUserAccountsEnabled = YES;
// SOGoCreateIdentitiesDisabled = NO;
SOGoMailCustomFromEnabled = YES;
SOGoMailingMechanism = smtp;
SOGoSMTPAuthenticationType = plain;
SxVMemLimit = 384;
SOGoMaximumPingInterval = 3540;
SOGoInternalSyncInterval = 45;
SOGoMaximumSyncInterval = 3540;
// 100 seems to break some Android clients
//SOGoMaximumSyncWindowSize = 99;
// This should do the trick for Outlook 2016
SOGoMaximumSyncResponseSize = 512;
WOWatchDogRequestTimeout = 30;
WOListenQueueSize = 16;
WONoDetach = YES;
SOGoIMAPAclConformsToIMAPExt = Yes;
SOGoPageTitle = "SOGo Groupware";
SOGoFirstDayOfWeek = "1";
SOGoSieveFolderEncoding = "UTF-8";
SOGoPasswordChangeEnabled = NO;
SOGoSentFolderName = "Sent";
SOGoMailShowSubscribedFoldersOnly = NO;
NGImap4ConnectionStringSeparator = "/";
SOGoSieveScriptsEnabled = YES;
SOGoTrashFolderName = "Trash";
SOGoVacationEnabled = YES;
SOGoCacheCleanupInterval = 900;
SOGoMaximumFailedLoginCount = 10;
SOGoMaximumFailedLoginInterval = 900;
SOGoFailedLoginBlockInterval = 900;
GCSChannelCollectionTimer = 60;
GCSChannelExpireAge = 60;
MySQL4Encoding = "utf8mb4";
//SOGoDebugRequests = YES;
//SoDebugBaseURL = YES;
//ImapDebugEnabled = YES;
//SOGoEASDebugEnabled = YES;
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10
//LDAPDebugEnabled = YES;
//PGDebugEnabled = YES;
//MySQL4DebugEnabled = YES;
//SOGoUIxDebugEnabled = YES;
//WODontZipResponse = YES;
WOLogFile = "/dev/sogo_log";
}

45
data/conf/unbound/unbound.conf Executable file
View file

@ -0,0 +1,45 @@
server:
verbosity: 1
interface: 0.0.0.0
interface: ::0
logfile: /dev/console
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
do-daemonize: no
#access-control: 0.0.0.0/0 allow
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 192.168.0.0/16 allow
access-control: fc00::/7 allow
access-control: fe80::/10 allow
#access-control: ::0/0 allow
directory: "/etc/unbound"
username: unbound
auto-trust-anchor-file: trusted-key.key
#private-address: 10.0.0.0/8
#private-address: 172.16.0.0/12
#private-address: 192.168.0.0/16
#private-address: 169.254.0.0/16
#private-address: fc00::/7
#private-address: fe80::/10
# cache-min-ttl needs to be less or equal to cache-max-negative-ttl
cache-min-ttl: 5
cache-max-negative-ttl: 60
root-hints: "/etc/unbound/root.hints"
hide-identity: yes
hide-version: yes
max-udp-size: 4096
msg-buffer-size: 65552
unwanted-reply-threshold: 10000
ipsecmod-enabled: no
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
server-key-file: "/etc/unbound/unbound_server.key"
server-cert-file: "/etc/unbound/unbound_server.pem"
control-key-file: "/etc/unbound/unbound_control.key"
control-cert-file: "/etc/unbound/unbound_control.pem"