mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 08:24:37 +00:00
Compare commits
4 Commits
scripts-te
...
link2xt/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abd7e42a34 | ||
|
|
7c5ec1e0df | ||
|
|
11ebc4623c | ||
|
|
cf29053389 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -159,3 +159,5 @@ cython_debug/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
chatmail.zone
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ comprised of a minimal setup of the battle-tested
|
|||||||
|
|
||||||
scripts/init.sh
|
scripts/init.sh
|
||||||
|
|
||||||
2. set environment variable to the chatmail domain you want to setup:
|
2. setup a domain with `A` and `AAAA` records for your chatmail server
|
||||||
|
|
||||||
|
3. set environment variable to the chatmail domain you want to setup:
|
||||||
|
|
||||||
export CHATMAIL_DOMAIN=c1.testrun.org # replace with your host
|
export CHATMAIL_DOMAIN=c1.testrun.org # replace with your host
|
||||||
|
|
||||||
3. run the deploy of the chat mail instance:
|
4. run the deploy of the chat mail instance:
|
||||||
|
|
||||||
scripts/deploy.sh
|
scripts/deploy.sh
|
||||||
|
|
||||||
|
5. run `scripts/generate-dns-zone.sh` and create the generated DNS records at your DNS provider
|
||||||
|
|
||||||
## Running tests and benchmarks (offline and online)
|
## Running tests and benchmarks (offline and online)
|
||||||
|
|
||||||
|
|||||||
@@ -21,15 +21,27 @@ def encrypt_password(password: str):
|
|||||||
return "{SHA512-CRYPT}" + passhash
|
return "{SHA512-CRYPT}" + passhash
|
||||||
|
|
||||||
|
|
||||||
def create_user(db, user, password):
|
def check_password(password) -> bool:
|
||||||
|
"""Check password policy"""
|
||||||
|
if len(password) < 10:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(db, user, encrypted_password):
|
||||||
if os.path.exists(NOCREATE_FILE):
|
if os.path.exists(NOCREATE_FILE):
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation."
|
f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
with db.write_transaction() as conn:
|
with db.write_transaction() as conn:
|
||||||
conn.create_user(user, password)
|
conn.create_user(user, encrypted_password)
|
||||||
return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
|
return dict(
|
||||||
|
home=f"/home/vmail/{user}",
|
||||||
|
uid="vmail",
|
||||||
|
gid="vmail",
|
||||||
|
password=encrypted_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_user_data(db, user):
|
def get_user_data(db, user):
|
||||||
@@ -48,6 +60,9 @@ def lookup_userdb(db, user):
|
|||||||
def lookup_passdb(db, user, password):
|
def lookup_passdb(db, user, password):
|
||||||
userdata = get_user_data(db, user)
|
userdata = get_user_data(db, user)
|
||||||
if not userdata:
|
if not userdata:
|
||||||
|
if not check_password(password):
|
||||||
|
logging.warning("Attempt to create an account with a weak password.")
|
||||||
|
return
|
||||||
return create_user(db, user, encrypt_password(password))
|
return create_user(db, user, encrypt_password(password))
|
||||||
userdata["password"] = userdata["password"].strip()
|
userdata["password"] = userdata["password"].strip()
|
||||||
return userdata
|
return userdata
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ Chat Mail pyinfra deploy.
|
|||||||
import importlib.resources
|
import importlib.resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pyinfra import host, logger
|
from pyinfra import host
|
||||||
from pyinfra.operations import apt, files, server, systemd, python
|
from pyinfra.operations import apt, files, server, systemd
|
||||||
from pyinfra.facts.files import File
|
from pyinfra.facts.files import File
|
||||||
from .acmetool import deploy_acmetool
|
from .acmetool import deploy_acmetool
|
||||||
|
|
||||||
@@ -70,6 +70,36 @@ def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
|
|||||||
mode="644",
|
mode="644",
|
||||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||||
)
|
)
|
||||||
|
need_restart |= main_config.changed
|
||||||
|
|
||||||
|
files.directory(
|
||||||
|
name="Add opendkim directory to /etc",
|
||||||
|
path="/etc/opendkim",
|
||||||
|
user="opendkim",
|
||||||
|
group="opendkim",
|
||||||
|
mode="750",
|
||||||
|
present=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
keytable = files.template(
|
||||||
|
src=importlib.resources.files(__package__).joinpath("opendkim/KeyTable"),
|
||||||
|
dest="/etc/dkimkeys/KeyTable",
|
||||||
|
user="opendkim",
|
||||||
|
group="opendkim",
|
||||||
|
mode="644",
|
||||||
|
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||||
|
)
|
||||||
|
need_restart |= keytable.changed
|
||||||
|
|
||||||
|
signing_table = files.template(
|
||||||
|
src=importlib.resources.files(__package__).joinpath("opendkim/SigningTable"),
|
||||||
|
dest="/etc/dkimkeys/SigningTable",
|
||||||
|
user="opendkim",
|
||||||
|
group="opendkim",
|
||||||
|
mode="644",
|
||||||
|
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||||
|
)
|
||||||
|
need_restart |= signing_table.changed
|
||||||
|
|
||||||
files.directory(
|
files.directory(
|
||||||
name="Add opendkim socket directory to /var/spool/postfix",
|
name="Add opendkim socket directory to /var/spool/postfix",
|
||||||
@@ -90,8 +120,6 @@ def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
|
|||||||
_sudo_user="opendkim",
|
_sudo_user="opendkim",
|
||||||
)
|
)
|
||||||
|
|
||||||
need_restart |= main_config.changed
|
|
||||||
|
|
||||||
return need_restart
|
return need_restart
|
||||||
|
|
||||||
|
|
||||||
@@ -292,14 +320,3 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
restarted=journald_conf,
|
restarted=journald_conf,
|
||||||
)
|
)
|
||||||
|
|
||||||
def callback():
|
|
||||||
result = server.shell(
|
|
||||||
commands=[
|
|
||||||
f"""sed 's/\tIN/ 600 IN/;s/\t(//;s/\"$//;s/^\t \"//g; s/ ).*//' """
|
|
||||||
f"""/etc/dkimkeys/{dkim_selector}.txt | tr --delete '\n'"""
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger.info(f"Add this TXT entry into DNS zone: {result.stdout}")
|
|
||||||
|
|
||||||
python.call(name="Print TXT entry for DKIM", function=callback)
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from deploy_chatmail import deploy_chatmail
|
|||||||
def main():
|
def main():
|
||||||
mail_domain = os.getenv("CHATMAIL_DOMAIN")
|
mail_domain = os.getenv("CHATMAIL_DOMAIN")
|
||||||
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
|
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
|
||||||
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023")
|
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "dkim")
|
||||||
|
|
||||||
assert mail_domain
|
assert mail_domain
|
||||||
assert mail_server
|
assert mail_server
|
||||||
|
|||||||
1
deploy-chatmail/src/deploy_chatmail/opendkim/KeyTable
Normal file
1
deploy-chatmail/src/deploy_chatmail/opendkim/KeyTable
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dkim._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/dkim.private
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*@{{ config.domain_name }} {{ config.opendkim_selector }}._domainkey.{{ config.domain_name }}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
# This is a basic configuration for signing and verifying. It can easily be
|
# OpenDKIM configuration.
|
||||||
# adapted to suit a basic installation. See opendkim.conf(5) and
|
|
||||||
# /usr/share/doc/opendkim/examples/opendkim.conf.sample for complete
|
|
||||||
# documentation of available configuration parameters.
|
|
||||||
|
|
||||||
Syslog yes
|
Syslog yes
|
||||||
SyslogSuccess yes
|
SyslogSuccess yes
|
||||||
@@ -21,7 +18,9 @@ OversignHeaders From
|
|||||||
# setup options can be found in /usr/share/doc/opendkim/README.opendkim.
|
# setup options can be found in /usr/share/doc/opendkim/README.opendkim.
|
||||||
Domain {{ config.domain_name }}
|
Domain {{ config.domain_name }}
|
||||||
Selector {{ config.opendkim_selector }}
|
Selector {{ config.opendkim_selector }}
|
||||||
KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private
|
KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private
|
||||||
|
KeyTable /etc/dkimkeys/KeyTable
|
||||||
|
SigningTable /etc/dkimkeys/SigningTable
|
||||||
|
|
||||||
# In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
|
# In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
|
||||||
# using a local socket with MTAs that access the socket as a non-privileged
|
# using a local socket with MTAs that access the socket as a non-privileged
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
|
|
||||||
export CHATMAIL_DOMAIN
|
|
||||||
|
|
||||||
chatmaild/venv/bin/python3 -m build -n --sdist chatmaild --outdir dist
|
echo -----------------------------------------
|
||||||
|
echo deploying to $CHATMAIL_DOMAIN
|
||||||
|
echo -----------------------------------------
|
||||||
|
|
||||||
deploy-chatmail/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" \
|
echo WARNING: in five seconds deploy to $CHATMAIL_DOMAIN starts
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
venv/bin/python3 -m build -n --sdist chatmaild --outdir dist
|
||||||
|
|
||||||
|
venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" \
|
||||||
deploy-chatmail/src/deploy_chatmail/deploy.py
|
deploy-chatmail/src/deploy_chatmail/deploy.py
|
||||||
|
|
||||||
rm -r dist/
|
rm -r dist/
|
||||||
|
|||||||
20
scripts/generate-dns-zone.sh
Executable file
20
scripts/generate-dns-zone.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
|
||||||
|
: ${CHATMAIL_SSH:=$CHATMAIL_DOMAIN}
|
||||||
|
|
||||||
|
set -e
|
||||||
|
SSH="ssh root@$CHATMAIL_SSH"
|
||||||
|
EMAIL="root@$CHATMAIL_DOMAIN"
|
||||||
|
ACME_ACCOUNT_URL="$($SSH -- acmetool account-url)"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
$CHATMAIL_DOMAIN. MX 10 $CHATMAIL_DOMAIN.
|
||||||
|
$CHATMAIL_DOMAIN. TXT "v=spf1 a:$CHATMAIL_DOMAIN -all"
|
||||||
|
_dmarc.$CHATMAIL_DOMAIN. TXT "v=DMARC1;p=reject;rua=mailto:$EMAIL;ruf=mailto:$EMAIL;fo=1;adkim=r;aspf=r"
|
||||||
|
_submission._tcp.$CHATMAIL_DOMAIN. SRV 0 1 587 $CHATMAIL_DOMAIN.
|
||||||
|
_submissions._tcp.$CHATMAIL_DOMAIN. SRV 0 1 465 $CHATMAIL_DOMAIN.
|
||||||
|
_imap._tcp.$CHATMAIL_DOMAIN. SRV 0 1 143 $CHATMAIL_DOMAIN.
|
||||||
|
_imaps._tcp.$CHATMAIL_DOMAIN. SRV 0 1 993 $CHATMAIL_DOMAIN.
|
||||||
|
$CHATMAIL_DOMAIN. IN CAA 0 issue "letsencrypt.org; accounturi=$ACME_ACCOUNT_URL"
|
||||||
|
EOF
|
||||||
|
$SSH opendkim-genzone -F | sed 's/^;.*$//;/^$/d'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
tox -c chatmaild
|
venv/bin/tox -c chatmaild
|
||||||
tox -c deploy-chatmail
|
venv/bin/tox -c deploy-chatmail
|
||||||
venv/bin/pytest tests/online -vrx --durations=5 $@
|
venv/bin/pytest tests/online -vrx --durations=5 $@
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def test_basic(db):
|
|||||||
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
|
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
|
||||||
if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE):
|
if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE):
|
||||||
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
|
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
|
||||||
lookup_passdb(db, "link2xt@c1.testrun.org", "asdf")
|
lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
|
||||||
data = get_user_data(db, "link2xt@c1.testrun.org")
|
data = get_user_data(db, "link2xt@c1.testrun.org")
|
||||||
assert data
|
assert data
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ def test_nocreate_file(db):
|
|||||||
with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f:
|
with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f:
|
||||||
f.write("")
|
f.write("")
|
||||||
assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE)
|
assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE)
|
||||||
lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
|
lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik")
|
||||||
assert not get_user_data(db, "newuser1@something.org")
|
assert not get_user_data(db, "newuser1@something.org")
|
||||||
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
|
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
|||||||
with pytest.raises(imap_or_smtp.AuthError):
|
with pytest.raises(imap_or_smtp.AuthError):
|
||||||
imap_or_smtp.login(user, password + "wrong")
|
imap_or_smtp.login(user, password + "wrong")
|
||||||
|
|
||||||
|
lp.sec(f"creating users with a short password is not allowed")
|
||||||
|
user, _password = gencreds()
|
||||||
|
with pytest.raises(imap_or_smtp.AuthError):
|
||||||
|
imap_or_smtp.login(user, "admin")
|
||||||
|
|
||||||
|
|
||||||
def test_login_same_password(imap_or_smtp, gencreds):
|
def test_login_same_password(imap_or_smtp, gencreds):
|
||||||
"""Test two different users logging in with the same password
|
"""Test two different users logging in with the same password
|
||||||
|
|||||||
Reference in New Issue
Block a user