mirror of
				https://github.com/optim-enterprises-bv/Mailu.git
				synced 2025-10-30 17:47:55 +00: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 | ||||
| - **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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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"] | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
							
								
								
									
										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.call(["/config.py"]) | ||||
| os.system("dovecot -c /etc/dovecot/proxy.conf") | ||||
| 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. | ||||
|  | ||||
| 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 | ||||
| listening on these ports (or have them listen on a different port). For | ||||
| 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 | ||||
| - **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 | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
							
								
								
									
										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'; | ||||
|  | ||||
| // 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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Florent Daigniere
					Florent Daigniere