You've already forked Mailu
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:
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"]
|
||||||
|
@@ -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;
|
||||||
|
@@ -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
38
core/nginx/login.lua
Normal 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
62
core/nginx/proxy.conf
Normal 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 %}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;"])
|
||||||
|
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -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 %}
|
||||||
|
1
towncrier/newsfragments/81.feature
Normal file
1
towncrier/newsfragments/81.feature
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Add support for managesieve
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user