mirror of
https://github.com/chatmail/relay.git
synced 2026-05-13 17:34:38 +00:00
Compare commits
13 Commits
link2xt/do
...
link2xt/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3cb73585 | ||
|
|
75b41641f0 | ||
|
|
30a61972fb | ||
|
|
bcc54602ee | ||
|
|
f9998d5721 | ||
|
|
8605ceba5e | ||
|
|
30bcf9ff77 | ||
|
|
70b0e9d5e5 | ||
|
|
fdd533aa3b | ||
|
|
a44ed0aeb3 | ||
|
|
f5bfa6bd56 | ||
|
|
81a6f8808b | ||
|
|
be3685519f |
@@ -151,10 +151,12 @@ While this file is present, account creation will be blocked.
|
|||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
|
[Postfix](http://www.postfix.org/) listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
|
||||||
Dovecot listens on ports 143(imap) and 993 (imaps).
|
[Dovecot](https://www.dovecot.org/) listens on ports 143 (imap) and 993 (imaps).
|
||||||
|
[nginx](https://www.nginx.com/) listens on port 443 (https).
|
||||||
|
[acmetool](https://hlandau.github.io/acmetool/) listens on port 80 (http).
|
||||||
|
|
||||||
Delta Chat apps will, however, discover all ports and configurations
|
Delta Chat apps will, however, discover all ports and configurations
|
||||||
automatically by reading the `autoconfig.xml` file from the chatmail service.
|
automatically by reading the [autoconfig XML file](https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html) from the chatmail service.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,13 +91,39 @@ def lookup_passdb(db, config: Config, user, cleartext_password):
|
|||||||
VALUES (?, ?, ?)"""
|
VALUES (?, ?, ?)"""
|
||||||
conn.execute(q, (user, encrypted_password, int(time.time())))
|
conn.execute(q, (user, encrypted_password, int(time.time())))
|
||||||
return dict(
|
return dict(
|
||||||
home=f"/home/vmail/{user}",
|
home=f"/home/vmail/mail/{config.mail_domain}/{user}",
|
||||||
uid="vmail",
|
uid="vmail",
|
||||||
gid="vmail",
|
gid="vmail",
|
||||||
password=encrypted_password,
|
password=encrypted_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def split_and_unescape(s):
|
||||||
|
"""Split strings using double quote as a separator and backslash as escape character
|
||||||
|
into parts."""
|
||||||
|
|
||||||
|
out = ""
|
||||||
|
i = 0
|
||||||
|
while i < len(s):
|
||||||
|
c = s[i]
|
||||||
|
if c == "\\":
|
||||||
|
# Skip escape character.
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# This will raise IndexError if there is no character
|
||||||
|
# after escape character. This is expected
|
||||||
|
# as this is an invalid input.
|
||||||
|
out += s[i]
|
||||||
|
elif c == '"':
|
||||||
|
# Separator
|
||||||
|
yield out
|
||||||
|
out = ""
|
||||||
|
else:
|
||||||
|
out += c
|
||||||
|
i += 1
|
||||||
|
yield out
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_request(msg, db, config: Config):
|
def handle_dovecot_request(msg, db, config: Config):
|
||||||
short_command = msg[0]
|
short_command = msg[0]
|
||||||
if short_command == "L": # LOOKUP
|
if short_command == "L": # LOOKUP
|
||||||
@@ -107,7 +133,9 @@ def handle_dovecot_request(msg, db, config: Config):
|
|||||||
# do not attempt to read any other parts for compatibility.
|
# do not attempt to read any other parts for compatibility.
|
||||||
keyname = parts[0]
|
keyname = parts[0]
|
||||||
|
|
||||||
namespace, type, *args = keyname.split("/")
|
namespace, type, args = keyname.split("/", 2)
|
||||||
|
args = list(split_and_unescape(args))
|
||||||
|
|
||||||
reply_command = "F"
|
reply_command = "F"
|
||||||
res = ""
|
res = ""
|
||||||
if namespace == "shared":
|
if namespace == "shared":
|
||||||
|
|||||||
@@ -52,15 +52,19 @@ def test_too_high_db_version(db):
|
|||||||
|
|
||||||
|
|
||||||
def test_handle_dovecot_request(db, example_config):
|
def test_handle_dovecot_request(db, example_config):
|
||||||
|
# Test that password can contain ", ', \ and /
|
||||||
msg = (
|
msg = (
|
||||||
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
|
'Lshared/passdb/laksjdlaksjdlak\\\\sjdlk\\"12j\\\'3l1/k2j3123"'
|
||||||
"some42123@chat.example.org\tsome42123@chat.example.org"
|
"some42123@chat.example.org\tsome42123@chat.example.org"
|
||||||
)
|
)
|
||||||
res = handle_dovecot_request(msg, db, example_config)
|
res = handle_dovecot_request(msg, db, example_config)
|
||||||
assert res
|
assert res
|
||||||
assert res[0] == "O" and res.endswith("\n")
|
assert res[0] == "O" and res.endswith("\n")
|
||||||
userdata = json.loads(res[1:].strip())
|
userdata = json.loads(res[1:].strip())
|
||||||
assert userdata["home"] == "/home/vmail/some42123@chat.example.org"
|
assert (
|
||||||
|
userdata["home"]
|
||||||
|
== "/home/vmail/mail/chat.example.org/some42123@chat.example.org"
|
||||||
|
)
|
||||||
assert userdata["uid"] == userdata["gid"] == "vmail"
|
assert userdata["uid"] == userdata["gid"] == "vmail"
|
||||||
assert userdata["password"].startswith("{SHA512-CRYPT}")
|
assert userdata["password"].startswith("{SHA512-CRYPT}")
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,15 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
|
|||||||
)
|
)
|
||||||
need_restart |= master_config.changed
|
need_restart |= master_config.changed
|
||||||
|
|
||||||
|
header_cleanup = files.put(
|
||||||
|
src=importlib.resources.files(__package__).joinpath("postfix/submission_header_cleanup"),
|
||||||
|
dest="/etc/postfix/submission_header_cleanup",
|
||||||
|
user="root",
|
||||||
|
group="root",
|
||||||
|
mode="644",
|
||||||
|
)
|
||||||
|
need_restart |= header_cleanup.changed
|
||||||
|
|
||||||
return need_restart
|
return need_restart
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import importlib.resources
|
import importlib.resources
|
||||||
|
|
||||||
from pyinfra.operations import apt, files, systemd, server
|
from pyinfra.operations import apt, files, systemd, server
|
||||||
|
from pyinfra import host
|
||||||
|
from pyinfra.facts.systemd import SystemdStatus
|
||||||
|
|
||||||
|
|
||||||
def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
||||||
@@ -55,6 +57,13 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
|||||||
group="root",
|
group="root",
|
||||||
mode="644",
|
mode="644",
|
||||||
)
|
)
|
||||||
|
if host.get_fact(SystemdStatus).get("nginx.service"):
|
||||||
|
systemd.service(
|
||||||
|
name="Stop nginx service to free port 80",
|
||||||
|
service="nginx",
|
||||||
|
running=False,
|
||||||
|
)
|
||||||
|
|
||||||
systemd.service(
|
systemd.service(
|
||||||
name="Setup acmetool-redirector service",
|
name="Setup acmetool-redirector service",
|
||||||
service="acmetool-redirector.service",
|
service="acmetool-redirector.service",
|
||||||
|
|||||||
@@ -37,21 +37,15 @@ class DNS:
|
|||||||
|
|
||||||
def get(self, typ: str, domain: str) -> str | None:
|
def get(self, typ: str, domain: str) -> str | None:
|
||||||
"""Get a DNS entry"""
|
"""Get a DNS entry"""
|
||||||
dig_result = self.shell(f"dig {typ} {domain}")
|
dig_result = self.shell(f"dig -r -q {domain} -t {typ} +short")
|
||||||
line_num = 0
|
line = dig_result.partition("\n")[0]
|
||||||
for line in dig_result.splitlines():
|
if line:
|
||||||
line_num += 1
|
return line
|
||||||
if line.strip() == ";; ANSWER SECTION:":
|
|
||||||
return dig_result.splitlines()[line_num].split("\t")[-1]
|
|
||||||
|
|
||||||
def check_ptr_record(self, ip: str, mail_domain) -> str:
|
def check_ptr_record(self, ip: str, mail_domain) -> bool:
|
||||||
"""Check the PTR record for an IPv4 or IPv6 address."""
|
"""Check the PTR record for an IPv4 or IPv6 address."""
|
||||||
result = self.get("-x", ip)
|
result = self.shell(f"dig -r -x {ip} +short").rstrip()
|
||||||
if result:
|
return result == f"{mail_domain}."
|
||||||
if ip_address(ip).version == 6:
|
|
||||||
result = result.split()[-1]
|
|
||||||
if result[:-1] == mail_domain:
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def show_dns(args, out):
|
def show_dns(args, out):
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
uri = proxy:/run/dovecot/doveauth.socket:auth
|
uri = proxy:/run/dovecot/doveauth.socket:auth
|
||||||
iterate_disable = yes
|
iterate_disable = yes
|
||||||
default_pass_scheme = plain
|
default_pass_scheme = plain
|
||||||
password_key = passdb/%w/%u
|
# %E escapes characters " (double quote), ' (single quote) and \ (backslash) with \ (backslash).
|
||||||
user_key = userdb/%u
|
# See <https://doc.dovecot.org/configuration_manual/config_file/config_variables/#modifiers>
|
||||||
|
# for documentation.
|
||||||
|
#
|
||||||
|
# We escape user-provided input and use double quote as a separator.
|
||||||
|
password_key = passdb/%Ew"%Eu
|
||||||
|
user_key = userdb/%Eu
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ append_dot_mydomain = no
|
|||||||
|
|
||||||
readme_directory = no
|
readme_directory = no
|
||||||
|
|
||||||
# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 2 on
|
# See http://www.postfix.org/COMPATIBILITY_README.html
|
||||||
# fresh installs.
|
compatibility_level = 3.6
|
||||||
compatibility_level = 2
|
|
||||||
|
|
||||||
# TLS parameters
|
# TLS parameters
|
||||||
smtpd_tls_cert_file=/var/lib/acme/live/{{ config.mail_domain }}/fullchain
|
smtpd_tls_cert_file=/var/lib/acme/live/{{ config.mail_domain }}/fullchain
|
||||||
@@ -49,3 +48,5 @@ virtual_mailbox_domains = {{ config.mail_domain }}
|
|||||||
|
|
||||||
smtpd_milters = unix:opendkim/opendkim.sock
|
smtpd_milters = unix:opendkim/opendkim.sock
|
||||||
non_smtpd_milters = $smtpd_milters
|
non_smtpd_milters = $smtpd_milters
|
||||||
|
|
||||||
|
header_checks = regexp:/etc/postfix/submission_header_cleanup
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ smtp inet n - y - - smtpd -v
|
|||||||
{% else %}
|
{% else %}
|
||||||
smtp inet n - y - - smtpd
|
smtp inet n - y - - smtpd
|
||||||
{% endif %}
|
{% endif %}
|
||||||
#smtp inet n - y - 1 postscreen
|
|
||||||
#smtpd pass - - y - - smtpd
|
|
||||||
#dnsblog unix - - y - 0 dnsblog
|
|
||||||
#tlsproxy unix - - y - 0 tlsproxy
|
|
||||||
submission inet n - y - - smtpd
|
submission inet n - y - - smtpd
|
||||||
-o syslog_name=postfix/submission
|
-o syslog_name=postfix/submission
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
|
|||||||
4
cmdeploy/src/cmdeploy/postfix/submission_header_cleanup
Normal file
4
cmdeploy/src/cmdeploy/postfix/submission_header_cleanup
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/^Received:/ IGNORE
|
||||||
|
/^X-Originating-IP:/ IGNORE
|
||||||
|
/^X-Mailer:/ IGNORE
|
||||||
|
/^User-Agent:/ IGNORE
|
||||||
@@ -14,3 +14,12 @@ def test_fastcgi_working(maildomain, chatmail_config):
|
|||||||
res = requests.post(url)
|
res = requests.post(url)
|
||||||
assert maildomain in res.json().get("email")
|
assert maildomain in res.json().get("email")
|
||||||
assert len(res.json().get("password")) > chatmail_config.password_min_length
|
assert len(res.json().get("password")) > chatmail_config.password_min_length
|
||||||
|
|
||||||
|
|
||||||
|
def test_newemail_configure(maildomain, rpc):
|
||||||
|
"""Test configuring accounts by scanning a QR code works."""
|
||||||
|
url = f"DCACCOUNT:https://{maildomain}/cgi-bin/newemail.py"
|
||||||
|
for i in range(3):
|
||||||
|
account_id = rpc.add_account()
|
||||||
|
rpc.set_config_from_qr(account_id, url)
|
||||||
|
rpc.configure(account_id)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
|
||||||
class TestEndToEndDeltaChat:
|
class TestEndToEndDeltaChat:
|
||||||
@@ -119,3 +122,17 @@ class TestEndToEndDeltaChat:
|
|||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
assert "error" not in m.get_message_info()
|
assert "error" not in m.get_message_info()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hide_senders_ip_address(cmfactory):
|
||||||
|
public_ip = requests.get("http://icanhazip.com").content.decode().strip()
|
||||||
|
assert ipaddress.ip_address(public_ip)
|
||||||
|
|
||||||
|
user1, user2 = cmfactory.get_online_accounts(2)
|
||||||
|
chat = cmfactory.get_accepted_chat(user1, user2)
|
||||||
|
|
||||||
|
chat.send_text("testing submission header cleanup")
|
||||||
|
user2.wait_next_incoming_message()
|
||||||
|
user2.direct_imap.select_folder("Inbox")
|
||||||
|
msg = user2.direct_imap.get_all_messages()[0]
|
||||||
|
assert public_ip not in msg.obj.as_string()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
## More information
|
## More information
|
||||||
|
|
||||||
`nine.testrun.org` provides a low-maintenance, resource efficient and
|
{{ config.mail_domain }} provides a low-maintenance, resource efficient and
|
||||||
interoperable e-mail service for everyone. What's behind a `chatmail` is
|
interoperable e-mail service for everyone. What's behind a `chatmail` is
|
||||||
effectively a normal e-mail address just like any other but optimized
|
effectively a normal e-mail address just like any other but optimized
|
||||||
for the usage in chats, especially DeltaChat.
|
for the usage in chats, especially DeltaChat.
|
||||||
|
|||||||
Reference in New Issue
Block a user