1
0
mirror of https://github.com/Mailu/Mailu.git synced 2025-08-10 22:31:47 +02:00

Implement managesieve support

This commit is contained in:
Florent Daigniere
2023-04-20 15:36:17 +02:00
parent 152a2bf175
commit 107b0ab5ff
15 changed files with 134 additions and 38 deletions

View File

@@ -20,7 +20,7 @@ Main features include:
- **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients - **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients
- **Advanced email features**, aliases, domain aliases, custom routing - **Advanced email features**, aliases, domain aliases, custom routing
- **Web access**, multiple Webmails and administration interface - **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 - **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 - **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 - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing

View File

@@ -13,7 +13,8 @@ STATUSES = {
"authentication": ("Authentication credentials invalid", { "authentication": ("Authentication credentials invalid", {
"imap": "AUTHENTICATIONFAILED", "imap": "AUTHENTICATIONFAILED",
"smtp": "535 5.7.8", "smtp": "535 5.7.8",
"pop3": "-ERR Authentication failed" "pop3": "-ERR Authentication failed",
"sieve": "AuthFailed"
}), }),
"encryption": ("Must issue a STARTTLS command first", { "encryption": ("Must issue a STARTTLS command first", {
"smtp": "530 5.7.0" "smtp": "530 5.7.0"
@@ -32,7 +33,7 @@ def check_credentials(user, password, ip, protocol=None, auth_port=None):
return False return False
is_ok = False is_ok = False
# webmails # 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): if utils.verify_temp_token(user.get_id(), password):
is_ok = True is_ok = True
# All tokens are 32 characters hex lowercase # All tokens are 32 characters hex lowercase
@@ -50,8 +51,8 @@ def handle_authentication(headers):
""" Handle an HTTP nginx authentication request """ Handle an HTTP nginx authentication request
See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol
""" """
method = headers["Auth-Method"] method = headers["Auth-Method"].lower()
protocol = headers["Auth-Protocol"] protocol = headers["Auth-Protocol"].lower()
# Incoming mail, no authentication # Incoming mail, no authentication
if method == "none" and protocol == "smtp": if method == "none" and protocol == "smtp":
server, port = get_server(protocol, False) server, port = get_server(protocol, False)
@@ -121,7 +122,7 @@ def handle_authentication(headers):
"Auth-Wait": 0 "Auth-Wait": 0
} }
# Unexpected # Unexpected
return {} raise Exception("SHOULD NOT HAPPEN")
def get_status(protocol, status): def get_status(protocol, status):
@@ -140,6 +141,8 @@ def get_server(protocol, authenticated=False):
hostname, port = app.config['SMTP_ADDRESS'], 10025 hostname, port = app.config['SMTP_ADDRESS'], 10025
else: else:
hostname, port = app.config['SMTP_ADDRESS'], 25 hostname, port = app.config['SMTP_ADDRESS'], 25
elif protocol == "sieve":
hostname, port = app.config['IMAP_ADDRESS'], 4190
try: try:
# test if hostname is already resolved to an ip address # test if hostname is already resolved to an ip address
ipaddress.ip_address(hostname) ipaddress.ip_address(hostname)

View File

@@ -12,6 +12,7 @@ default_login_user = mail
default_internal_group = dovecot default_internal_group = dovecot
haproxy_trusted_networks = {{ SUBNET }} {{ SUBNET6 }} haproxy_trusted_networks = {{ SUBNET }} {{ SUBNET6 }}
login_trusted_networks = {{ SUBNET }} {{ SUBNET6 }}
############### ###############
# Mailboxes # Mailboxes
@@ -149,7 +150,6 @@ service lmtp {
service managesieve-login { service managesieve-login {
inet_listener sieve { inet_listener sieve {
port = 4190 port = 4190
haproxy = yes
} }
} }

View File

@@ -17,17 +17,17 @@ ARG VERSION
LABEL version=$VERSION LABEL version=$VERSION
RUN set -euxo pipefail \ RUN set -euxo pipefail \
; apk add --no-cache certbot nginx nginx-mod-http-brotli nginx-mod-stream nginx-mod-mail openssl \ ; apk add --no-cache certbot nginx nginx-mod-http-brotli nginx-mod-mail openssl dovecot-lua dovecot-pigeonhole-plugin
; rm /etc/nginx/conf.d/stream.conf
COPY conf/ /conf/ COPY conf/ /conf/
COPY --from=static /static/ /static/ COPY --from=static /static/ /static/
COPY *.py / COPY *.py /
COPY proxy.conf login.lua /
RUN echo $VERSION >/version RUN echo $VERSION >/version
EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/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 14190/tcp # EXPOSE 10025/tcp 10143/tcp
HEALTHCHECK --start-period=60s CMD curl -skfLo /dev/null http://127.0.0.1:10204/health HEALTHCHECK --start-period=60s CMD curl -skfLo /dev/null http://127.0.0.1:10204/health
VOLUME ["/certs", "/overrides"] VOLUME ["/certs", "/overrides"]

View File

@@ -5,7 +5,6 @@ pcre_jit on;
error_log /dev/stderr notice; error_log /dev/stderr notice;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
load_module "modules/ngx_mail_module.so"; load_module "modules/ngx_mail_module.so";
load_module "modules/ngx_stream_module.so";
events { events {
worker_connections 1024; worker_connections 1024;
@@ -302,25 +301,6 @@ http {
include /etc/nginx/conf.d/*.conf; 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 { mail {
server_name {{ HOSTNAMES.split(",")[0] }}; server_name {{ HOSTNAMES.split(",")[0] }};
auth_http http://127.0.0.1:8000/auth/email; auth_http http://127.0.0.1:8000/auth/email;

View File

@@ -55,3 +55,7 @@ conf.jinja("/conf/proxy.conf", args, "/etc/nginx/proxy.conf")
conf.jinja("/conf/nginx.conf", args, "/etc/nginx/nginx.conf") conf.jinja("/conf/nginx.conf", args, "/etc/nginx/nginx.conf")
if os.path.exists("/var/run/nginx.pid"): if os.path.exists("/var/run/nginx.pid"):
os.system("nginx -s reload") 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")

38
core/nginx/login.lua Normal file
View File

@@ -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

62
core/nginx/proxy.conf Normal file
View File

@@ -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 = </conf/dhparam.pem
{% else %}
disable_plaintext_auth = no
protocol sieve {
ssl = no
}
{% endif %}
passdb {
driver = lua
args = file=/etc/dovecot/login.lua blocking=yes
}
service auth-worker {
user = $default_internal_user
group = $default_internal_group
}
service managesieve-login {
executable = managesieve-login
inet_listener sieve {
port = 4190
{% if PROXY_PROTOCOL in ['all', 'mail'] %}
haproxy = yes
{% endif %}
}
}

View File

@@ -13,4 +13,5 @@ elif os.environ["TLS_FLAVOR"] in [ "mail", "cert" ]:
subprocess.Popen(["/certwatcher.py"]) subprocess.Popen(["/certwatcher.py"])
subprocess.call(["/config.py"]) subprocess.call(["/config.py"])
os.system("dovecot -c /etc/dovecot/proxy.conf")
os.execv("/usr/sbin/nginx", ["nginx", "-g", "daemon off;"]) os.execv("/usr/sbin/nginx", ["nginx", "-g", "daemon off;"])

View File

@@ -63,7 +63,7 @@ address for your mail server and that you have a dedicated hostname
with forward and reverse DNS entries for this IP address. with forward and reverse DNS entries for this IP address.
Also, your host must not listen on ports ``25``, ``80``, ``110``, ``143``, Also, your host must not listen on ports ``25``, ``80``, ``110``, ``143``,
``443``, ``465``, ``587``, ``993`` or ``995`` as these are used by Mailu ``443``, ``465``, ``587``, ``993``, ``995`` nor ``4190`` as these are used by Mailu
services. Therefore, you should disable or uninstall any program that is services. Therefore, you should disable or uninstall any program that is
listening on these ports (or have them listen on a different port). For listening on these ports (or have them listen on a different port). For
instance, on a default Debian install: instance, on a default Debian install:

View File

@@ -26,7 +26,7 @@ Main features include:
- **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients - **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients
- **Advanced email features**, aliases, domain aliases, custom routing - **Advanced email features**, aliases, domain aliases, custom routing
- **Web access**, multiple Webmails and administration interface - **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 - **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 - **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 - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing

View File

@@ -30,7 +30,7 @@ services:
options: options:
tag: mailu-front tag: mailu-front
ports: 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 %} {% if bind4 %}
- "{{ bind4 }}:{{ port }}:{{ port }}" - "{{ bind4 }}:{{ port }}:{{ port }}"
{% endif %} {% endif %}

View File

@@ -0,0 +1 @@
Add support for managesieve

View File

@@ -24,7 +24,14 @@ $config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p'; $config['smtp_pass'] = '%p';
// Sieve script management // 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'; $config['managesieve_mbox_encoding'] = 'UTF8';
// roundcube customization // roundcube customization

View File

@@ -33,13 +33,13 @@
}, },
"Sieve": { "Sieve": {
"host": "{{ FRONT_ADDRESS }}", "host": "{{ FRONT_ADDRESS }}",
"port": 14190, "port": 4190,
"secure": 0, "type": 2,
"shortLogin": false, "shortLogin": false,
"ssl": { "ssl": {
"verify_peer": false, "verify_peer": false,
"verify_peer_name": false, "verify_peer_name": false,
"allow_self_signed": false, "allow_self_signed": true,
"SNI_enabled": true, "SNI_enabled": true,
"disable_compression": true, "disable_compression": true,
"security_level": 1 "security_level": 1