mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 00:14:36 +00:00
Compare commits
1 Commits
link2xt/sc
...
favicon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290ffa13d2 |
20
README.md
20
README.md
@@ -25,56 +25,54 @@ DNS domain name (FQDN), for example `chat.example.org`.
|
|||||||
ssh root@CHATMAIL_DOMAIN
|
ssh root@CHATMAIL_DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install the `cmdeploy` command in a virtualenv
|
2. Install the `cmdeploy` command in a virtualenv
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/deltachat/chatmail
|
source scripts/initenv.sh
|
||||||
cd chatmail
|
|
||||||
scripts/initenv.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create chatmail configuration file `chatmail.ini`:
|
3. Create chatmail configuration file `chatmail.ini`:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy init CHATMAIL_DOMAIN
|
cmdeploy init CHATMAIL_DOMAIN
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Deploy to the remote chatmail server:
|
4. Deploy to the remote chatmail server:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy run
|
cmdeploy run
|
||||||
```
|
```
|
||||||
|
|
||||||
5. To output a DNS zone file from which you can transfer DNS records
|
5. To output a DNS zone file from which you can transfer DNS records
|
||||||
to your DNS provider:
|
to your DNS provider:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy dns
|
cmdeploy dns
|
||||||
```
|
```
|
||||||
|
|
||||||
6. To check status of your remotely running chatmail service:
|
6. To check status of your remotely running chatmail service:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy status
|
cmdeploy status
|
||||||
```
|
```
|
||||||
|
|
||||||
7. To test your chatmail service:
|
7. To test your chatmail service:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy test
|
cmdeploy test
|
||||||
```
|
```
|
||||||
|
|
||||||
8. To benchmark your chatmail service:
|
8. To benchmark your chatmail service:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy bench
|
cmdeploy bench
|
||||||
```
|
```
|
||||||
|
|
||||||
### Refining the web pages
|
### Refining the web pages
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy webdev
|
cmdeploy webdev
|
||||||
```
|
```
|
||||||
|
|
||||||
This starts a local live development cycle for chatmail Web pages:
|
This starts a local live development cycle for chatmail Web pages:
|
||||||
|
|||||||
@@ -11,15 +11,9 @@ class Config:
|
|||||||
self._inipath = inipath
|
self._inipath = inipath
|
||||||
self.mail_domain = params["mail_domain"]
|
self.mail_domain = params["mail_domain"]
|
||||||
self.max_user_send_per_minute = int(params["max_user_send_per_minute"])
|
self.max_user_send_per_minute = int(params["max_user_send_per_minute"])
|
||||||
self.max_mailbox_size = params["max_mailbox_size"]
|
|
||||||
self.delete_mails_after = params["delete_mails_after"]
|
|
||||||
self.username_min_length = int(params["username_min_length"])
|
|
||||||
self.username_max_length = int(params["username_max_length"])
|
|
||||||
self.password_min_length = int(params["password_min_length"])
|
|
||||||
self.passthrough_senders = params["passthrough_senders"].split()
|
|
||||||
self.passthrough_recipients = params["passthrough_recipients"].split()
|
|
||||||
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
||||||
self.postfix_reinject_port = int(params["postfix_reinject_port"])
|
self.postfix_reinject_port = int(params["postfix_reinject_port"])
|
||||||
|
self.passthrough_recipients = params["passthrough_recipients"].split()
|
||||||
self.privacy_postal = params.get("privacy_postal")
|
self.privacy_postal = params.get("privacy_postal")
|
||||||
self.privacy_mail = params.get("privacy_mail")
|
self.privacy_mail = params.get("privacy_mail")
|
||||||
self.privacy_pdo = params.get("privacy_pdo")
|
self.privacy_pdo = params.get("privacy_pdo")
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from socketserver import (
|
|||||||
import pwd
|
import pwd
|
||||||
|
|
||||||
from .database import Database
|
from .database import Database
|
||||||
from .config import read_config, Config
|
|
||||||
|
|
||||||
NOCREATE_FILE = "/etc/chatmail-nocreate"
|
NOCREATE_FILE = "/etc/chatmail-nocreate"
|
||||||
|
|
||||||
@@ -23,17 +22,14 @@ def encrypt_password(password: str):
|
|||||||
return "{SHA512-CRYPT}" + passhash
|
return "{SHA512-CRYPT}" + passhash
|
||||||
|
|
||||||
|
|
||||||
def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
|
def is_allowed_to_create(user, cleartext_password) -> bool:
|
||||||
"""Return True if user and password are admissable."""
|
"""Return True if user and password are admissable."""
|
||||||
if os.path.exists(NOCREATE_FILE):
|
if os.path.exists(NOCREATE_FILE):
|
||||||
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
|
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(cleartext_password) < config.password_min_length:
|
if len(cleartext_password) < 9:
|
||||||
logging.warning(
|
logging.warning("Password needs to be at least 9 characters long")
|
||||||
"Password needs to be at least %s characters long",
|
|
||||||
config.password_min_length,
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
parts = user.split("@")
|
parts = user.split("@")
|
||||||
@@ -42,17 +38,11 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
|
|||||||
return False
|
return False
|
||||||
localpart, domain = parts
|
localpart, domain = parts
|
||||||
|
|
||||||
if (
|
if domain == "nine.testrun.org":
|
||||||
len(localpart) > config.username_max_length
|
# nine.testrun.org policy, username has to be exactly nine chars
|
||||||
or len(localpart) < config.username_min_length
|
if len(localpart) != 9:
|
||||||
):
|
logging.warning(f"localpart {localpart!r} has not exactly nine chars")
|
||||||
logging.warning(
|
return False
|
||||||
"localpart %s has to be between %s and %s chars long",
|
|
||||||
localpart,
|
|
||||||
config.username_min_length,
|
|
||||||
config.username_max_length,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -70,7 +60,7 @@ def lookup_userdb(db, user):
|
|||||||
return get_user_data(db, user)
|
return get_user_data(db, user)
|
||||||
|
|
||||||
|
|
||||||
def lookup_passdb(db, config: Config, user, cleartext_password):
|
def lookup_passdb(db, user, cleartext_password):
|
||||||
with db.write_transaction() as conn:
|
with db.write_transaction() as conn:
|
||||||
userdata = conn.get_user(user)
|
userdata = conn.get_user(user)
|
||||||
if userdata:
|
if userdata:
|
||||||
@@ -82,7 +72,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password):
|
|||||||
userdata["uid"] = "vmail"
|
userdata["uid"] = "vmail"
|
||||||
userdata["gid"] = "vmail"
|
userdata["gid"] = "vmail"
|
||||||
return userdata
|
return userdata
|
||||||
if not is_allowed_to_create(config, user, cleartext_password):
|
if not is_allowed_to_create(user, cleartext_password):
|
||||||
return
|
return
|
||||||
|
|
||||||
encrypted_password = encrypt_password(cleartext_password)
|
encrypted_password = encrypt_password(cleartext_password)
|
||||||
@@ -97,7 +87,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_request(msg, db, config: Config):
|
def handle_dovecot_request(msg, db, mail_domain):
|
||||||
short_command = msg[0]
|
short_command = msg[0]
|
||||||
if short_command == "L": # LOOKUP
|
if short_command == "L": # LOOKUP
|
||||||
parts = msg[1:].split("\t")
|
parts = msg[1:].split("\t")
|
||||||
@@ -107,15 +97,15 @@ def handle_dovecot_request(msg, db, config: Config):
|
|||||||
res = ""
|
res = ""
|
||||||
if namespace == "shared":
|
if namespace == "shared":
|
||||||
if type == "userdb":
|
if type == "userdb":
|
||||||
if user.endswith(f"@{config.mail_domain}"):
|
if user.endswith(f"@{mail_domain}"):
|
||||||
res = lookup_userdb(db, user)
|
res = lookup_userdb(db, user)
|
||||||
if res:
|
if res:
|
||||||
reply_command = "O"
|
reply_command = "O"
|
||||||
else:
|
else:
|
||||||
reply_command = "N"
|
reply_command = "N"
|
||||||
elif type == "passdb":
|
elif type == "passdb":
|
||||||
if user.endswith(f"@{config.mail_domain}"):
|
if user.endswith(f"@{mail_domain}"):
|
||||||
res = lookup_passdb(db, config, user, cleartext_password=args[0])
|
res = lookup_passdb(db, user, cleartext_password=args[0])
|
||||||
if res:
|
if res:
|
||||||
reply_command = "O"
|
reply_command = "O"
|
||||||
else:
|
else:
|
||||||
@@ -133,7 +123,8 @@ def main():
|
|||||||
socket = sys.argv[1]
|
socket = sys.argv[1]
|
||||||
passwd_entry = pwd.getpwnam(sys.argv[2])
|
passwd_entry = pwd.getpwnam(sys.argv[2])
|
||||||
db = Database(sys.argv[3])
|
db = Database(sys.argv[3])
|
||||||
config = read_config(sys.argv[4])
|
with open("/etc/mailname", "r") as fp:
|
||||||
|
mail_domain = fp.read().strip()
|
||||||
|
|
||||||
class Handler(StreamRequestHandler):
|
class Handler(StreamRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
@@ -142,7 +133,7 @@ def main():
|
|||||||
msg = self.rfile.readline().strip().decode()
|
msg = self.rfile.readline().strip().decode()
|
||||||
if not msg:
|
if not msg:
|
||||||
break
|
break
|
||||||
res = handle_dovecot_request(msg, db, config)
|
res = handle_dovecot_request(msg, db, mail_domain)
|
||||||
if res:
|
if res:
|
||||||
self.wfile.write(res.encode("ascii"))
|
self.wfile.write(res.encode("ascii"))
|
||||||
self.wfile.flush()
|
self.wfile.flush()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Description=Chatmail dict authentication proxy for dovecot
|
Description=Chatmail dict authentication proxy for dovecot
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart={execpath} /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite {config_path}
|
ExecStart={execpath} /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
|
|
||||||
|
|||||||
@@ -111,9 +111,6 @@ class BeforeQueueHandler:
|
|||||||
if not mail_encrypted and check_mdn(message, envelope):
|
if not mail_encrypted and check_mdn(message, envelope):
|
||||||
return
|
return
|
||||||
|
|
||||||
if envelope.mail_from in self.config.passthrough_senders:
|
|
||||||
return
|
|
||||||
|
|
||||||
passthrough_recipients = self.config.passthrough_recipients
|
passthrough_recipients = self.config.passthrough_recipients
|
||||||
envelope_from_domain = from_addr.split("@").pop()
|
envelope_from_domain = from_addr.split("@").pop()
|
||||||
for recipient in envelope.rcpt_tos:
|
for recipient in envelope.rcpt_tos:
|
||||||
|
|||||||
@@ -7,48 +7,18 @@ mail_domain = {mail_domain}
|
|||||||
# If you only do private test deploys, you don't need to modify any settings below
|
# If you only do private test deploys, you don't need to modify any settings below
|
||||||
#
|
#
|
||||||
|
|
||||||
#
|
|
||||||
# Account Restrictions
|
|
||||||
#
|
|
||||||
|
|
||||||
# how many mails a user can send out per minute
|
# how many mails a user can send out per minute
|
||||||
max_user_send_per_minute = 60
|
max_user_send_per_minute = 60
|
||||||
|
|
||||||
# maximum mailbox size of a chatmail account
|
|
||||||
max_mailbox_size = 100M
|
|
||||||
|
|
||||||
# time after which seen mails are deleted
|
|
||||||
delete_mails_after = 40d
|
|
||||||
|
|
||||||
# minimum length a username must have
|
|
||||||
username_min_length = 9
|
|
||||||
|
|
||||||
# maximum length a username can have
|
|
||||||
username_max_length = 9
|
|
||||||
|
|
||||||
# minimum length a password must have
|
|
||||||
password_min_length = 9
|
|
||||||
|
|
||||||
# list of chatmail accounts which can send outbound un-encrypted mail
|
|
||||||
passthrough_senders =
|
|
||||||
|
|
||||||
# list of e-mail recipients for which to accept outbound un-encrypted mails
|
# list of e-mail recipients for which to accept outbound un-encrypted mails
|
||||||
passthrough_recipients =
|
passthrough_recipients =
|
||||||
|
|
||||||
#
|
|
||||||
# Deployment Details
|
|
||||||
#
|
|
||||||
|
|
||||||
# where the filtermail SMTP service listens
|
# where the filtermail SMTP service listens
|
||||||
filtermail_smtp_port = 10080
|
filtermail_smtp_port = 10080
|
||||||
|
|
||||||
# postfix accepts on the localhost reinject SMTP port
|
# postfix accepts on the localhost reinject SMTP port
|
||||||
postfix_reinject_port = 10025
|
postfix_reinject_port = 10025
|
||||||
|
|
||||||
#
|
|
||||||
# Privacy Policy
|
|
||||||
#
|
|
||||||
|
|
||||||
# postal address of privacy contact
|
# postal address of privacy contact
|
||||||
privacy_postal =
|
privacy_postal =
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
#!/usr/local/lib/chatmaild/venv/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
""" CGI script for creating new accounts. """
|
""" CGI script for creating new accounts. """
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from chatmaild.config import read_config, Config
|
mail_domain_path = "/etc/mailname"
|
||||||
|
|
||||||
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
|
|
||||||
|
|
||||||
|
|
||||||
def create_newemail_dict(config: Config):
|
def create_newemail_dict(domain):
|
||||||
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||||
user = "".join(random.choices(alphanumeric, k=config.username_min_length))
|
user = "".join(random.choices(alphanumeric, k=9))
|
||||||
password = "".join(random.choices(alphanumeric, k=config.password_min_length + 3))
|
password = "".join(random.choices(alphanumeric, k=12))
|
||||||
return dict(email=f"{user}@{config.mail_domain}", password=f"{password}")
|
return dict(email=f"{user}@{domain}", password=f"{password}")
|
||||||
|
|
||||||
|
|
||||||
def print_new_account():
|
def print_new_account():
|
||||||
config = read_config(CONFIG_PATH)
|
domain = open(mail_domain_path).read().strip()
|
||||||
creds = create_newemail_dict(config)
|
creds = create_newemail_dict(domain=domain)
|
||||||
|
|
||||||
print("Content-Type: application/json")
|
print("Content-Type: application/json")
|
||||||
print("")
|
print("")
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
|
|
||||||
|
|
||||||
def test_read_config_basic(example_config):
|
def test_read_config_basic(make_config):
|
||||||
assert example_config.mail_domain == "chat.example.org"
|
config = make_config("chat.example.org")
|
||||||
assert not example_config.privacy_supervisor and not example_config.privacy_mail
|
assert config.mail_domain == "chat.example.org"
|
||||||
assert not example_config.privacy_pdo and not example_config.privacy_postal
|
assert not config.privacy_supervisor and not config.privacy_mail
|
||||||
|
assert not config.privacy_pdo and not config.privacy_postal
|
||||||
|
|
||||||
inipath = example_config._inipath
|
inipath = config._inipath
|
||||||
inipath.write_text(inipath.read_text().replace("60", "37"))
|
inipath.write_text(inipath.read_text().replace("60", "37"))
|
||||||
example_config = read_config(inipath)
|
config = read_config(inipath)
|
||||||
assert example_config.max_user_send_per_minute == 37
|
assert config.max_user_send_per_minute == 37
|
||||||
assert example_config.mail_domain == "chat.example.org"
|
assert config.mail_domain == "chat.example.org"
|
||||||
|
|
||||||
|
|
||||||
def test_read_config_testrun(make_config):
|
def test_read_config_testrun(make_config):
|
||||||
@@ -23,10 +24,4 @@ def test_read_config_testrun(make_config):
|
|||||||
assert config.filtermail_smtp_port == 10080
|
assert config.filtermail_smtp_port == 10080
|
||||||
assert config.postfix_reinject_port == 10025
|
assert config.postfix_reinject_port == 10025
|
||||||
assert config.max_user_send_per_minute == 60
|
assert config.max_user_send_per_minute == 60
|
||||||
assert config.max_mailbox_size == "100M"
|
assert config.passthrough_recipients
|
||||||
assert config.delete_mails_after == "40d"
|
|
||||||
assert config.username_min_length == 9
|
|
||||||
assert config.username_max_length == 9
|
|
||||||
assert config.password_min_length == 9
|
|
||||||
assert config.passthrough_recipients == ["privacy@testrun.org"]
|
|
||||||
assert config.passthrough_senders == []
|
|
||||||
|
|||||||
@@ -9,35 +9,29 @@ from chatmaild.doveauth import get_user_data, lookup_passdb, handle_dovecot_requ
|
|||||||
from chatmaild.database import DBError
|
from chatmaild.database import DBError
|
||||||
|
|
||||||
|
|
||||||
def test_basic(db, example_config):
|
def test_basic(db):
|
||||||
lookup_passdb(db, example_config, "asdf12345@chat.example.org", "q9mr3faue")
|
lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
|
||||||
data = get_user_data(db, "asdf12345@chat.example.org")
|
data = get_user_data(db, "link2xt@c1.testrun.org")
|
||||||
assert data
|
assert data
|
||||||
data2 = lookup_passdb(
|
data2 = lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
|
||||||
db, example_config, "asdf12345@chat.example.org", "q9mr3jewvadsfaue"
|
|
||||||
)
|
|
||||||
assert data == data2
|
assert data == data2
|
||||||
|
|
||||||
|
|
||||||
def test_dont_overwrite_password_on_wrong_login(db, example_config):
|
def test_dont_overwrite_password_on_wrong_login(db):
|
||||||
"""Test that logging in with a different password doesn't create a new user"""
|
"""Test that logging in with a different password doesn't create a new user"""
|
||||||
res = lookup_passdb(
|
res = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
|
||||||
db, example_config, "newuser12@chat.example.org", "kajdlkajsldk12l3kj1983"
|
|
||||||
)
|
|
||||||
assert res["password"]
|
assert res["password"]
|
||||||
res2 = lookup_passdb(db, example_config, "newuser12@chat.example.org", "kajdslqwe")
|
res2 = lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
|
||||||
# this function always returns a password hash, which is actually compared by dovecot.
|
# this function always returns a password hash, which is actually compared by dovecot.
|
||||||
assert res["password"] == res2["password"]
|
assert res["password"] == res2["password"]
|
||||||
|
|
||||||
|
|
||||||
def test_nocreate_file(db, monkeypatch, tmpdir, example_config):
|
def test_nocreate_file(db, monkeypatch, tmpdir):
|
||||||
p = tmpdir.join("nocreate")
|
p = tmpdir.join("nocreate")
|
||||||
p.write("")
|
p.write("")
|
||||||
monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p))
|
monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p))
|
||||||
lookup_passdb(
|
lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik")
|
||||||
db, example_config, "newuser12@chat.example.org", "zequ0Aimuchoodaechik"
|
assert not get_user_data(db, "newuser1@something.org")
|
||||||
)
|
|
||||||
assert not get_user_data(db, "newuser12@chat.example.org")
|
|
||||||
|
|
||||||
|
|
||||||
def test_db_version(db):
|
def test_db_version(db):
|
||||||
@@ -51,21 +45,21 @@ def test_too_high_db_version(db):
|
|||||||
db.ensure_tables()
|
db.ensure_tables()
|
||||||
|
|
||||||
|
|
||||||
def test_handle_dovecot_request(db, example_config):
|
def test_handle_dovecot_request(db):
|
||||||
msg = (
|
msg = (
|
||||||
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
|
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
|
||||||
"some42123@chat.example.org\tsome42123@chat.example.org"
|
"some42@c3.testrun.org\tsome42@c3.testrun.org"
|
||||||
)
|
)
|
||||||
res = handle_dovecot_request(msg, db, example_config)
|
res = handle_dovecot_request(msg, db, "c3.testrun.org")
|
||||||
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/some42@c3.testrun.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}")
|
||||||
|
|
||||||
|
|
||||||
def test_50_concurrent_lookups_different_accounts(db, gencreds, example_config):
|
def test_50_concurrent_lookups_different_accounts(db, gencreds):
|
||||||
num_threads = 50
|
num_threads = 50
|
||||||
req_per_thread = 5
|
req_per_thread = 5
|
||||||
results = queue.Queue()
|
results = queue.Queue()
|
||||||
@@ -74,7 +68,7 @@ def test_50_concurrent_lookups_different_accounts(db, gencreds, example_config):
|
|||||||
for i in range(req_per_thread):
|
for i in range(req_per_thread):
|
||||||
addr, password = gencreds()
|
addr, password = gencreds()
|
||||||
try:
|
try:
|
||||||
lookup_passdb(db, example_config, addr, password)
|
lookup_passdb(db, addr, password)
|
||||||
except Exception:
|
except Exception:
|
||||||
results.put(traceback.format_exc())
|
results.put(traceback.format_exc())
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -127,19 +127,3 @@ def test_excempt_privacy(maildata, gencreds, handler):
|
|||||||
content = msg.as_bytes()
|
content = msg.as_bytes()
|
||||||
|
|
||||||
assert "500" in handler.check_DATA(envelope=env2)
|
assert "500" in handler.check_DATA(envelope=env2)
|
||||||
|
|
||||||
|
|
||||||
def test_passthrough_senders(gencreds, handler, maildata):
|
|
||||||
acc1 = gencreds()[0]
|
|
||||||
to_addr = "recipient@something.org"
|
|
||||||
handler.config.passthrough_senders = [acc1]
|
|
||||||
|
|
||||||
msg = maildata("plain.eml", acc1, to_addr)
|
|
||||||
|
|
||||||
class env:
|
|
||||||
mail_from = acc1
|
|
||||||
rcpt_tos = to_addr
|
|
||||||
content = msg.as_bytes()
|
|
||||||
|
|
||||||
# assert that None/no error is returned
|
|
||||||
assert not handler.check_DATA(envelope=env)
|
|
||||||
|
|||||||
@@ -4,24 +4,26 @@ import chatmaild
|
|||||||
from chatmaild.newemail import create_newemail_dict, print_new_account
|
from chatmaild.newemail import create_newemail_dict, print_new_account
|
||||||
|
|
||||||
|
|
||||||
def test_create_newemail_dict(example_config):
|
def test_create_newemail_dict():
|
||||||
ac1 = create_newemail_dict(example_config)
|
ac1 = create_newemail_dict(domain="example.org")
|
||||||
assert "@" in ac1["email"]
|
assert "@" in ac1["email"]
|
||||||
assert len(ac1["password"]) >= 10
|
assert len(ac1["password"]) >= 10
|
||||||
|
|
||||||
ac2 = create_newemail_dict(example_config)
|
ac2 = create_newemail_dict(domain="example.org")
|
||||||
|
|
||||||
assert ac1["email"] != ac2["email"]
|
assert ac1["email"] != ac2["email"]
|
||||||
assert ac1["password"] != ac2["password"]
|
assert ac1["password"] != ac2["password"]
|
||||||
|
|
||||||
|
|
||||||
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_config):
|
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir):
|
||||||
monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath))
|
p = tmpdir.join("mailname")
|
||||||
|
p.write(maildomain)
|
||||||
|
monkeypatch.setattr(chatmaild.newemail, "mail_domain_path", str(p))
|
||||||
print_new_account()
|
print_new_account()
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
lines = out.split("\n")
|
lines = out.split("\n")
|
||||||
assert lines[0] == "Content-Type: application/json"
|
assert lines[0] == "Content-Type: application/json"
|
||||||
assert not lines[1]
|
assert not lines[1]
|
||||||
dic = json.loads(lines[2])
|
dic = json.loads(lines[2])
|
||||||
assert dic["email"].endswith(f"@{example_config.mail_domain}")
|
assert dic["email"].endswith(f"@{maildomain}")
|
||||||
assert len(dic["password"]) >= 10
|
assert len(dic["password"]) >= 10
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
|
|||||||
return need_restart
|
return need_restart
|
||||||
|
|
||||||
|
|
||||||
def _configure_dovecot(config: Config, debug: bool = False) -> bool:
|
def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
|
||||||
"""Configures Dovecot IMAP server."""
|
"""Configures Dovecot IMAP server."""
|
||||||
need_restart = False
|
need_restart = False
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
|
|||||||
user="root",
|
user="root",
|
||||||
group="root",
|
group="root",
|
||||||
mode="644",
|
mode="644",
|
||||||
config=config,
|
config={"hostname": mail_server},
|
||||||
debug=debug,
|
debug=debug,
|
||||||
)
|
)
|
||||||
need_restart |= main_config.changed
|
need_restart |= main_config.changed
|
||||||
@@ -266,13 +266,14 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
|
|||||||
)
|
)
|
||||||
need_restart |= auth_config.changed
|
need_restart |= auth_config.changed
|
||||||
|
|
||||||
files.template(
|
files.put(
|
||||||
src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"),
|
src=importlib.resources.files(__package__)
|
||||||
|
.joinpath("dovecot/expunge.cron")
|
||||||
|
.open("rb"),
|
||||||
dest="/etc/cron.d/expunge",
|
dest="/etc/cron.d/expunge",
|
||||||
user="root",
|
user="root",
|
||||||
group="root",
|
group="root",
|
||||||
mode="644",
|
mode="644",
|
||||||
config=config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# as per https://doc.dovecot.org/configuration_manual/os/
|
# as per https://doc.dovecot.org/configuration_manual/os/
|
||||||
@@ -422,7 +423,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
|
|||||||
|
|
||||||
_install_remote_venv_with_chatmaild(config)
|
_install_remote_venv_with_chatmaild(config)
|
||||||
debug = False
|
debug = False
|
||||||
dovecot_need_restart = _configure_dovecot(config, debug=debug)
|
dovecot_need_restart = _configure_dovecot(mail_server, debug=debug)
|
||||||
postfix_need_restart = _configure_postfix(config, debug=debug)
|
postfix_need_restart = _configure_postfix(config, debug=debug)
|
||||||
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
|
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
|
||||||
mta_sts_need_restart = _install_mta_sts_daemon()
|
mta_sts_need_restart = _install_mta_sts_daemon()
|
||||||
|
|||||||
@@ -110,15 +110,6 @@ def status_cmd(args, out):
|
|||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
def test_cmd_options(parser):
|
|
||||||
parser.add_argument(
|
|
||||||
"--slow",
|
|
||||||
dest="slow",
|
|
||||||
action="store_true",
|
|
||||||
help="also run slow tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cmd(args, out):
|
def test_cmd(args, out):
|
||||||
"""Run local and online tests for chatmail deployment.
|
"""Run local and online tests for chatmail deployment.
|
||||||
|
|
||||||
@@ -130,10 +121,9 @@ def test_cmd(args, out):
|
|||||||
out.check_call(f"{sys.executable} -m pip install deltachat")
|
out.check_call(f"{sys.executable} -m pip install deltachat")
|
||||||
|
|
||||||
pytest_path = shutil.which("pytest")
|
pytest_path = shutil.which("pytest")
|
||||||
pytest_args = [pytest_path, "cmdeploy/src/", "-n4", "-rs", "-x", "-vrx", "--durations=5"]
|
ret = out.run_ret(
|
||||||
if args.slow:
|
[pytest_path, "cmdeploy/src/", "-n4", "-rs", "-x", "-vrx", "--durations=5"]
|
||||||
pytest_args.append("--slow")
|
)
|
||||||
ret = out.run_ret(pytest_args)
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ plugin {
|
|||||||
plugin {
|
plugin {
|
||||||
# for now we define static quota-rules for all users
|
# for now we define static quota-rules for all users
|
||||||
quota = maildir:User quota
|
quota = maildir:User quota
|
||||||
quota_rule = *:storage={{ config.max_mailbox_size }}
|
quota_rule = *:storage=100M
|
||||||
quota_max_mail_size=30M
|
quota_max_mail_size=30M
|
||||||
quota_grace = 0
|
quota_grace = 0
|
||||||
# quota_over_flag_value = TRUE
|
# quota_over_flag_value = TRUE
|
||||||
@@ -137,8 +137,8 @@ service imap-login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ssl = required
|
ssl = required
|
||||||
ssl_cert = </var/lib/acme/live/{{ config.mail_domain }}/fullchain
|
ssl_cert = </var/lib/acme/live/{{ config.hostname }}/fullchain
|
||||||
ssl_key = </var/lib/acme/live/{{ config.mail_domain }}/privkey
|
ssl_key = </var/lib/acme/live/{{ config.hostname }}/privkey
|
||||||
ssl_dh = </usr/share/dovecot/dh.pem
|
ssl_dh = </usr/share/dovecot/dh.pem
|
||||||
ssl_min_protocol = TLSv1.2
|
ssl_min_protocol = TLSv1.2
|
||||||
ssl_prefer_server_ciphers = yes
|
ssl_prefer_server_ciphers = yes
|
||||||
|
|||||||
4
cmdeploy/src/cmdeploy/dovecot/expunge.cron
Normal file
4
cmdeploy/src/cmdeploy/dovecot/expunge.cron
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE 40d INBOX
|
||||||
|
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE 40d Deltachat
|
||||||
|
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE 40d Trash
|
||||||
|
2 30 * * * dovecot doveadm purge -A
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} INBOX
|
|
||||||
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} Deltachat
|
|
||||||
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} Trash
|
|
||||||
2 30 * * * dovecot doveadm purge -A
|
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
import requests
|
|
||||||
|
|
||||||
from cmdeploy.genqr import gen_qr_png_data
|
from cmdeploy.genqr import gen_qr_png_data
|
||||||
|
|
||||||
|
|
||||||
def test_gen_qr_png_data(maildomain):
|
def test_gen_qr_png_data(maildomain):
|
||||||
data = gen_qr_png_data(maildomain)
|
data = gen_qr_png_data(maildomain)
|
||||||
assert data
|
assert data
|
||||||
|
|
||||||
|
|
||||||
def test_fastcgi_working(maildomain, chatmail_config):
|
|
||||||
url = f"https://{maildomain}/cgi-bin/newemail.py"
|
|
||||||
print(url)
|
|
||||||
res = requests.post(url)
|
|
||||||
assert maildomain in res.json().get("email")
|
|
||||||
assert len(res.json().get("password")) > chatmail_config.password_min_length
|
|
||||||
|
|||||||
@@ -43,18 +43,18 @@ def test_reject_forged_from(cmsetup, maildata, gencreds, lp, forgeaddr):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
def test_exceed_rate_limit(cmsetup, gencreds, maildata, chatmail_config):
|
def test_exceed_rate_limit(cmsetup, gencreds, maildata):
|
||||||
"""Test that the per-account send-mail limit is exceeded."""
|
"""Test that the per-account send-mail limit is exceeded."""
|
||||||
user1, user2 = cmsetup.gen_users(2)
|
user1, user2 = cmsetup.gen_users(2)
|
||||||
mail = maildata(
|
mail = maildata(
|
||||||
"encrypted.eml", from_addr=user1.addr, to_addr=user2.addr
|
"encrypted.eml", from_addr=user1.addr, to_addr=user2.addr
|
||||||
).as_string()
|
).as_string()
|
||||||
for i in range(chatmail_config.max_user_send_per_minute + 5):
|
for i in range(100):
|
||||||
print("Sending mail", str(i))
|
print("Sending mail", str(i))
|
||||||
try:
|
try:
|
||||||
user1.smtp.sendmail(user1.addr, [user2.addr], mail)
|
user1.smtp.sendmail(user1.addr, [user2.addr], mail)
|
||||||
except smtplib.SMTPException as e:
|
except smtplib.SMTPException as e:
|
||||||
if i < chatmail_config.max_user_send_per_minute:
|
if i < 60:
|
||||||
pytest.fail(f"rate limit was exceeded too early with msg {i}")
|
pytest.fail(f"rate limit was exceeded too early with msg {i}")
|
||||||
outcome = e.recipients[user2.addr]
|
outcome = e.recipients[user2.addr]
|
||||||
assert outcome[0] == 450
|
assert outcome[0] == 450
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import time
|
import time
|
||||||
import re
|
|
||||||
import random
|
import random
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -21,24 +20,14 @@ class TestEndToEndDeltaChat:
|
|||||||
assert msg2.text == "message0"
|
assert msg2.text == "message0"
|
||||||
|
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
def test_exceed_quota(self, cmfactory, lp, tmpdir, remote, chatmail_config):
|
def test_exceed_quota(self, cmfactory, lp, tmpdir, remote):
|
||||||
"""This is a very slow test as it needs to upload >100MB of mail data
|
"""This is a very slow test as it needs to upload >100MB of mail data
|
||||||
before quota is exceeded, and thus depends on the speed of the upload.
|
before quota is exceeded, and thus depends on the speed of the upload.
|
||||||
"""
|
"""
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
|
||||||
def parse_size_limit(limit: str) -> int:
|
quota = 1024 * 1024 * 100
|
||||||
"""Parse a size limit and return the number of bytes as integer.
|
|
||||||
|
|
||||||
Example input: 100M, 2.4T, 500 K
|
|
||||||
"""
|
|
||||||
units = {"B": 1, "K": 2**10, "M": 2**20, "G": 2**30, "T": 2**40}
|
|
||||||
size = re.sub(r'([KMGT])', r' \1', limit.upper())
|
|
||||||
number, unit = [string.strip() for string in size.split()]
|
|
||||||
return int(float(number) * units[unit])
|
|
||||||
|
|
||||||
quota = parse_size_limit(chatmail_config.max_mailbox_size)
|
|
||||||
attachsize = 1 * 1024 * 1024
|
attachsize = 1 * 1024 * 1024
|
||||||
num_to_send = quota // attachsize + 2
|
num_to_send = quota // attachsize + 2
|
||||||
lp.sec(f"ac1: send {num_to_send} large files to ac2")
|
lp.sec(f"ac1: send {num_to_send} large files to ac2")
|
||||||
|
|||||||
@@ -228,25 +228,18 @@ def imap_or_smtp(request):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def gencreds(chatmail_config):
|
def gencreds(maildomain):
|
||||||
count = itertools.count()
|
count = itertools.count()
|
||||||
next(count)
|
next(count)
|
||||||
|
|
||||||
def gen(domain=None):
|
def gen(domain=None):
|
||||||
domain = domain if domain else chatmail_config.mail_domain
|
domain = domain if domain else maildomain
|
||||||
while 1:
|
while 1:
|
||||||
num = next(count)
|
num = next(count)
|
||||||
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||||
user = "".join(
|
user = "".join(random.choices(alphanumeric, k=10))
|
||||||
random.choices(alphanumeric, k=chatmail_config.username_max_length)
|
user = f"ac{num}_{user}"[:9]
|
||||||
)
|
password = "".join(random.choices(alphanumeric, k=12))
|
||||||
if domain == "nine.testrun.org":
|
|
||||||
user = f"ac{num}_{user}"[:9]
|
|
||||||
else:
|
|
||||||
user = f"ac{num}_{user}"[: chatmail_config.username_max_length]
|
|
||||||
password = "".join(
|
|
||||||
random.choices(alphanumeric, k=chatmail_config.password_min_length)
|
|
||||||
)
|
|
||||||
yield f"{user}@{domain}", f"{password}"
|
yield f"{user}@{domain}", f"{password}"
|
||||||
|
|
||||||
return lambda domain=None: next(gen(domain))
|
return lambda domain=None: next(gen(domain))
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Wrapper for cmdelpoy to run it in activated virtualenv.
|
|
||||||
set -e
|
|
||||||
. venv/bin/activate
|
|
||||||
cmdeploy "$@"
|
|
||||||
@@ -4,3 +4,6 @@ python3 -m venv venv
|
|||||||
|
|
||||||
venv/bin/pip install -e chatmaild
|
venv/bin/pip install -e chatmaild
|
||||||
venv/bin/pip install -e cmdeploy
|
venv/bin/pip install -e cmdeploy
|
||||||
|
|
||||||
|
source venv/bin/activate
|
||||||
|
echo activated 'venv' python virtualenv environment containing "cmdeploy" tool
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<img class="banner" src="collage-top.png"/>
|
<img width="800px" src="collage-top.png"/>
|
||||||
|
|
||||||
## Dear [Delta Chat](https://get.delta.chat) users and newcomers,
|
## Dear [Delta Chat](https://get.delta.chat) users and newcomers,
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ Welcome to instant, interoperable and [privacy-preserving](privacy.html) messagi
|
|||||||
|
|
||||||
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
||||||
|
|
||||||
<div class="experimental">Note: this is an experimental service</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
## ⚡ Note: this is an experimental service ⚡
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<img class="banner" src="collage-info.png"/>
|
<img width="800px" src="collage-info.png"/>
|
||||||
|
|
||||||
## More information
|
## More information
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ The first login sets your password.
|
|||||||
|
|
||||||
- You may send up to 60 messages per minute
|
- You may send up to 60 messages per minute
|
||||||
|
|
||||||
- Seen messages are removed 40 days after arriving on the server
|
- Messages are unconditionally removed 40 days after arriving on the server
|
||||||
|
|
||||||
- You can store up to [100MB messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server)
|
- You can store up to [100MB messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server)
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
#menu {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu li {
|
|
||||||
display: inline-block;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#domain {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#domain a {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.experimental {
|
|
||||||
margin: 3em 0;
|
|
||||||
padding: 1em;
|
|
||||||
border: 4px dashed red;
|
|
||||||
color: red;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -7,21 +7,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<title>{{ config.mail_domain }} {{ pagename }}</title>
|
<title>{{ config.mail_domain }} {{ pagename }}</title>
|
||||||
<link rel="stylesheet" href="./water.css">
|
<link rel="stylesheet" href="./water.css">
|
||||||
<link rel="stylesheet" href="./main.css">
|
|
||||||
<link rel="icon" href="/logo.svg">
|
<link rel="icon" href="/logo.svg">
|
||||||
<link rel=”mask-icon” href=”/logo.svg” color=”#000000">
|
<link rel=”mask-icon” href=”/logo.svg” color=”#000000">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<ul id="menu">
|
|
||||||
<li><a href="index.html">home</a></li>
|
|
||||||
<li><a href="info.html">info</a></li>
|
|
||||||
<li><a href="privacy.html">privacy</a></li>
|
|
||||||
<li><a href="https://github.com/deltachat/chatmail">public code ↗</a></li>
|
|
||||||
<li id="domain"><a href="index.html">{{ config.mail_domain }}</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{{ markdown_html }}
|
{{ markdown_html }}
|
||||||
|
<footer>
|
||||||
|
<a href="index.html">home</a> |
|
||||||
|
<a href="info.html">more info</a> |
|
||||||
|
<a href="privacy.html">privacy</a> |
|
||||||
|
<a href="https://github.com/deltachat/chatmail">-> public development </a>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<img class="banner" src="collage-privacy.png"/>
|
<img width="800px" src="collage-privacy.png"/>
|
||||||
|
|
||||||
# Privacy Policy for {{ config.mail_domain }}
|
# Privacy Policy for {{ config.mail_domain }}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user