diff --git a/README.md b/README.md index 54045d28..6a202940 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Main features include: - **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients - **Advanced email features**, aliases, domain aliases, custom routing - **Web access**, multiple Webmails and administration interface -- **User features**, aliases, auto-reply, auto-forward, fetched accounts +- **User features**, aliases, auto-reply, auto-forward, fetched accounts, managesieve - **Admin features**, global admins, announcements, per-domain delegation, quotas - **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/), block malicious attachments - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index d0d11052..6596944b 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -13,7 +13,8 @@ STATUSES = { "authentication": ("Authentication credentials invalid", { "imap": "AUTHENTICATIONFAILED", "smtp": "535 5.7.8", - "pop3": "-ERR Authentication failed" + "pop3": "-ERR Authentication failed", + "sieve": "AuthFailed" }), "encryption": ("Must issue a STARTTLS command first", { "smtp": "530 5.7.0" @@ -32,7 +33,7 @@ def check_credentials(user, password, ip, protocol=None, auth_port=None): return False is_ok = False # webmails - if auth_port in WEBMAIL_PORTS and password.startswith('token-'): + if auth_port in WEBMAIL_PORTS or auth_port == '4190' and password.startswith('token-'): if utils.verify_temp_token(user.get_id(), password): is_ok = True # All tokens are 32 characters hex lowercase @@ -50,8 +51,8 @@ def handle_authentication(headers): """ Handle an HTTP nginx authentication request See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol """ - method = headers["Auth-Method"] - protocol = headers["Auth-Protocol"] + method = headers["Auth-Method"].lower() + protocol = headers["Auth-Protocol"].lower() # Incoming mail, no authentication if method == "none" and protocol == "smtp": server, port = get_server(protocol, False) @@ -121,7 +122,7 @@ def handle_authentication(headers): "Auth-Wait": 0 } # Unexpected - return {} + raise Exception("SHOULD NOT HAPPEN") def get_status(protocol, status): @@ -140,6 +141,8 @@ def get_server(protocol, authenticated=False): hostname, port = app.config['SMTP_ADDRESS'], 10025 else: hostname, port = app.config['SMTP_ADDRESS'], 25 + elif protocol == "sieve": + hostname, port = app.config['IMAP_ADDRESS'], 4190 try: # test if hostname is already resolved to an ip address ipaddress.ip_address(hostname) diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index ebcd97aa..9b75574c 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -12,6 +12,7 @@ default_login_user = mail default_internal_group = dovecot haproxy_trusted_networks = {{ SUBNET }} {{ SUBNET6 }} +login_trusted_networks = {{ SUBNET }} {{ SUBNET6 }} ############### # Mailboxes @@ -149,7 +150,6 @@ service lmtp { service managesieve-login { inet_listener sieve { port = 4190 - haproxy = yes } } diff --git a/core/nginx/Dockerfile b/core/nginx/Dockerfile index ae6f40d2..7f60a466 100644 --- a/core/nginx/Dockerfile +++ b/core/nginx/Dockerfile @@ -17,17 +17,17 @@ ARG VERSION LABEL version=$VERSION RUN set -euxo pipefail \ - ; apk add --no-cache certbot nginx nginx-mod-http-brotli nginx-mod-stream nginx-mod-mail openssl \ - ; rm /etc/nginx/conf.d/stream.conf + ; apk add --no-cache certbot nginx nginx-mod-http-brotli nginx-mod-mail openssl dovecot-lua dovecot-pigeonhole-plugin COPY conf/ /conf/ COPY --from=static /static/ /static/ COPY *.py / +COPY proxy.conf login.lua / RUN echo $VERSION >/version -EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp -# EXPOSE 10025/tcp 10143/tcp 14190/tcp +EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 4190/tcp +# EXPOSE 10025/tcp 10143/tcp HEALTHCHECK --start-period=60s CMD curl -skfLo /dev/null http://127.0.0.1:10204/health VOLUME ["/certs", "/overrides"] diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index deadbc57..2bb4d98f 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -5,7 +5,6 @@ pcre_jit on; error_log /dev/stderr notice; pid /var/run/nginx.pid; load_module "modules/ngx_mail_module.so"; -load_module "modules/ngx_stream_module.so"; events { worker_connections 1024; @@ -302,25 +301,6 @@ http { include /etc/nginx/conf.d/*.conf; } -stream { - log_format main '$remote_addr [$time_local] ' - '$protocol $status $bytes_sent $bytes_received ' - '$session_time "$upstream_addr" ' - '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"'; - access_log /dev/stdout main; - - # managesieve - server { - listen 14190; - resolver {{ RESOLVER }} valid=30s; - - proxy_connect_timeout 1s; - proxy_timeout 1m; - proxy_protocol on; - proxy_pass {{ IMAP_ADDRESS }}:4190; - } -} - mail { server_name {{ HOSTNAMES.split(",")[0] }}; auth_http http://127.0.0.1:8000/auth/email; diff --git a/core/nginx/config.py b/core/nginx/config.py index 94bb26b0..b5ff8590 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -55,3 +55,7 @@ conf.jinja("/conf/proxy.conf", args, "/etc/nginx/proxy.conf") conf.jinja("/conf/nginx.conf", args, "/etc/nginx/nginx.conf") if os.path.exists("/var/run/nginx.pid"): os.system("nginx -s reload") +conf.jinja("/login.lua", args, "/etc/dovecot/login.lua") +conf.jinja("/proxy.conf", args, "/etc/dovecot/proxy.conf") +if os.path.exists("/run/dovecot/master.pid"): + os.system("doveadm reload") diff --git a/core/nginx/login.lua b/core/nginx/login.lua new file mode 100644 index 00000000..7bb42e1e --- /dev/null +++ b/core/nginx/login.lua @@ -0,0 +1,38 @@ +function script_init() + return 0 +end + +function script_deinit() +end + +local http_client = dovecot.http.client { + timeout = 2000; + max_attempts = 3; +} + +function auth_passdb_lookup(req) + local auth_request = http_client:request { + url = "http://{{ ADMIN_ADDRESS }}/internal/auth/email"; + } + auth_request:add_header('Auth-Port', req.local_port) + auth_request:add_header('Auth-User', req.user) + auth_request:add_header('Auth-Pass', req.password) + auth_request:add_header('Auth-Protocol', 'sieve') + auth_request:add_header('Client-IP', req.remote_ip) + auth_request:add_header('Auth-SSL', req.secured) + auth_request:add_header('Auth-Method', req.mechanism) + local auth_response = auth_request:submit() + local resp_status = auth_response:status() + + if resp_status == 200 + then + if auth_response:header('Auth-Status') == 'OK' + then + return dovecot.auth.PASSDB_RESULT_OK, "proxy=y host={{ IMAP_ADDRESS }} nopassword=Y" + else + return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "" + end + else + return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "" + end +end diff --git a/core/nginx/proxy.conf b/core/nginx/proxy.conf new file mode 100644 index 00000000..94743fe5 --- /dev/null +++ b/core/nginx/proxy.conf @@ -0,0 +1,62 @@ +############### +# General +############### +log_path = /dev/stderr +protocols = sieve +postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }} +hostname = {{ HOSTNAMES.split(",")[0] }} +submission_host = {{ FRONT_ADDRESS }} +#instance_name = managesieveproxy +#base_dir = /run/dovecot2 + +default_internal_user = dovecot +default_login_user = mail +default_internal_group = dovecot + +haproxy_trusted_networks = {% if REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %}{{ from_ip }} {% endfor %}{% endif %} + +############### +# Authentication +############### +auth_username_chars = +auth_mechanisms = plain login + +{% if TLS %} +ssl = required +ssl_cert = <{{ TLS[0] }} +ssl_key = <{{ TLS[1] }} +{% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} +ssl_alt_cert = <{{ TLS[2] }} +ssl_alt_key = <{{ TLS[3] }} +{% endif %} +# intermediate configuration +ssl_min_protocol = TLSv1.2 +ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 +ssl_prefer_server_ciphers = no +ssl_dh = `_, block malicious attachments - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index c83ff29e..b266fec0 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -30,7 +30,7 @@ services: options: tag: mailu-front ports: - {% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %} + {% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993, 4190) %} {% if bind4 %} - "{{ bind4 }}:{{ port }}:{{ port }}" {% endif %} diff --git a/towncrier/newsfragments/81.feature b/towncrier/newsfragments/81.feature new file mode 100644 index 00000000..323a3742 --- /dev/null +++ b/towncrier/newsfragments/81.feature @@ -0,0 +1 @@ +Add support for managesieve diff --git a/webmails/roundcube/config/config.inc.php b/webmails/roundcube/config/config.inc.php index 3af8c38c..f5399a2d 100644 --- a/webmails/roundcube/config/config.inc.php +++ b/webmails/roundcube/config/config.inc.php @@ -24,7 +24,14 @@ $config['smtp_user'] = '%u'; $config['smtp_pass'] = '%p'; // Sieve script management -$config['managesieve_host'] = '{{ FRONT_ADDRESS or "front" }}:14190'; +$config['managesieve_host'] = 'tls://{{ FRONT_ADDRESS or "front" }}:4190'; +$config['managesieve_conn_options'] = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ), +); $config['managesieve_mbox_encoding'] = 'UTF8'; // roundcube customization diff --git a/webmails/snappymail/defaults/default.json b/webmails/snappymail/defaults/default.json index 0d49bfb4..96bd3a7d 100644 --- a/webmails/snappymail/defaults/default.json +++ b/webmails/snappymail/defaults/default.json @@ -33,13 +33,13 @@ }, "Sieve": { "host": "{{ FRONT_ADDRESS }}", - "port": 14190, - "secure": 0, + "port": 4190, + "type": 2, "shortLogin": false, "ssl": { "verify_peer": false, "verify_peer_name": false, - "allow_self_signed": false, + "allow_self_signed": true, "SNI_enabled": true, "disable_compression": true, "security_level": 1