Compare commits

..

32 Commits

Author SHA1 Message Date
missytake
4b9b0f5f44 lint: fix issues 2024-01-18 17:26:24 +01:00
link2xt
10c671ebda Reject on DKIM PERMFAIL and SPF PERMFAIL as well 2024-01-18 17:24:36 +01:00
link2xt
f2be32ac6f Fixup rspamd disabled.conf deployment message 2024-01-18 17:24:36 +01:00
link2xt
b702848c33 Replace rspamd rule weights with a strict rule 2024-01-18 17:24:36 +01:00
link2xt
a6f2f74520 Remove unused _configure_opendkim 2024-01-18 17:24:36 +01:00
link2xt
01ec341364 Disable ratelimit module like other modules 2024-01-18 17:24:36 +01:00
link2xt
998799fe3f Do not return anything from remove_opendkim() 2024-01-18 17:24:36 +01:00
link2xt
6186dc5259 Actually disable phising, rbl and hfilter 2024-01-18 17:24:36 +01:00
missytake
5880133b5b rspamd: remove redis (not needed) 2024-01-18 17:24:36 +01:00
missytake
6772bfe630 lint fixes, final touch 2024-01-18 17:24:36 +01:00
missytake
101c3a6b47 rspamd: reject emails with invalid SPF, DKIM, DMARC 2024-01-18 17:24:36 +01:00
missytake
5ef2100765 tests: use generic recipient for DKIM testing 2024-01-18 17:24:36 +01:00
missytake
d49aae365c revert "Significantly lower ratelimit" 2024-01-18 17:24:36 +01:00
missytake
998a185332 rspamd: generate DKIM keys with rspamadm 2024-01-18 17:24:36 +01:00
missytake
3e78555ca1 rspamd: install rspamd + redis 2024-01-18 17:24:36 +01:00
missytake
01cfd0be19 tests: add test for rejecting SPF & DMARC fails 2024-01-18 17:24:36 +01:00
missytake
1bdc547479 lint: fix 3 issues 2024-01-18 17:24:36 +01:00
missytake
c0b8ba816d rspamd: Significantly lower ratelimit; without read receipts this should be more than fine 2024-01-18 17:24:36 +01:00
missytake
118ae49674 rspamd: add redis-server for caching 2024-01-18 17:24:36 +01:00
missytake
a47df20e22 rspamd: disable RBL checks 2024-01-18 17:24:36 +01:00
missytake
a1d8881887 rspamd: add rate limiting 2024-01-18 17:24:36 +01:00
missytake
cd7416a0dd disable some unnecessary rspamd modules 2024-01-18 17:24:36 +01:00
missytake
173e3f6390 do DKIM signing with rspamd instead of openDKIM 2024-01-18 17:24:36 +01:00
missytake
b8d53242cf DNS: added www subdomain to zonefile 2024-01-18 17:24:36 +01:00
link2xt
c65f618fb1 nginx: redirect www. to non-www 2024-01-18 17:24:36 +01:00
link2xt
42afad0852 Fix indentation in nginx.conf.j2 2024-01-18 17:24:36 +01:00
link2xt
8bc19439a9 dns: require www. subdomain and request TLS certificate for it 2024-01-18 17:24:36 +01:00
link2xt
cdaddb9b0f dns: check mta-sts CNAME directly without resolving to IP 2024-01-18 17:24:36 +01:00
missytake
768bf2b22c greeterbot: better comparison method
Co-authored-by: holger krekel  <holger@merlinux.eu>
2024-01-18 17:09:23 +01:00
missytake
185e6f7d2a greeterbot: address hpk's comments 2024-01-13 17:37:22 +01:00
missytake
90e7169eef lint: fix issues 2024-01-12 16:24:45 +01:00
missytake
3db7933d8b greeterbot: port to chatmail 2024-01-12 16:20:39 +01:00
13 changed files with 189 additions and 77 deletions

View File

@@ -10,6 +10,10 @@ dependencies = [
"iniconfig",
"deltachat-rpc-server",
"deltachat-rpc-client",
"ConfigArgParse",
"deltachat",
"setuptools>=60",
"setuptools-scm>=8",
]
[tool.setuptools]
@@ -22,6 +26,7 @@ where = ['src']
doveauth = "chatmaild.doveauth:main"
filtermail = "chatmaild.filtermail:main"
echobot = "chatmaild.echo:main"
greeterbot = "chatmaild.greeterbot:main"
chatmail-metrics = "chatmaild.metrics:main"
[project.entry-points.pytest11]

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -46,11 +46,17 @@ class Connection:
)
return result
def get_user_list(self) -> set[str]:
"""Get a set of all users."""
q = "SELECT addr from users"
return set([tup[0] for tup in self._sqlconn.execute(q).fetchall()])
class Database:
def __init__(self, path: str):
def __init__(self, path: str, read_only=False):
self.path = Path(path)
self.ensure_tables()
if not read_only:
self.ensure_tables()
def _get_connection(
self, write=False, transaction=False, closing=False

View File

@@ -46,7 +46,7 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
len(localpart) > config.username_max_length
or len(localpart) < config.username_min_length
):
if localpart != "echo":
if localpart not in ("echo", "hello"):
logging.warning(
"localpart %s has to be between %s and %s chars long",
localpart,

Binary file not shown.

View File

@@ -0,0 +1,132 @@
import time
import deltachat
from deltachat.tracker import ConfigureFailed
from time import sleep
import tempfile
import os
import configargparse
import pkg_resources
import secrets
from chatmaild.database import Database
from chatmaild.config import read_config
from chatmaild.newemail import ALPHANUMERIC_PUNCT, CONFIG_PATH
PASSDB_PATH = "/home/vmail/passdb.sqlite"
def setup_account(data_dir: str, debug: bool) -> deltachat.Account:
"""Create a deltachat account with a given addr/password combination.
:param data_dir: the directory where the data(base) is stored.
:param debug: whether to show log messages for the account.
:return: the deltachat account object.
"""
chatmail_config = read_config(CONFIG_PATH)
addr = "hello@" + chatmail_config.mail_domain
try:
os.mkdir(os.path.join(data_dir, addr))
except FileExistsError:
pass
db_path = os.path.join(data_dir, addr, "db.sqlite")
ac = deltachat.Account(db_path)
if debug:
ac.add_account_plugin(deltachat.events.FFIEventLogger(ac))
ac.set_config("mvbox_move", "0")
ac.set_config("sentbox_watch", "0")
ac.set_config("bot", "1")
ac.set_config("mdns_enabled", "0")
if not ac.is_configured():
cleartext_password = "".join(
secrets.choice(ALPHANUMERIC_PUNCT)
for _ in range(chatmail_config.password_min_length + 3)
)
ac.set_config("mail_pw", cleartext_password)
ac.set_config("addr", addr)
configtracker = ac.configure()
try:
configtracker.wait_finish()
except ConfigureFailed:
print(
"configuration setup failed for %s with password:\n%s"
% (ac.get_config("addr"), ac.get_config("mail_pw"))
)
raise
ac.start_io()
avatar = pkg_resources.resource_filename(__name__, "avatar.jpg")
ac.set_avatar(avatar)
ac.set_config("displayname", f"Hello at {chatmail_config.mail_domain}!")
return ac
class GreetBot:
def __init__(self, passdb, account):
self.db = Database(passdb, read_only=True)
self.account = account
self.domain = account.get_config("addr").split("@")[1]
with self.db.read_connection() as conn:
self.existing_users = conn.get_user_list()
def greet_users(self):
with self.db.read_connection() as conn:
users = conn.get_user_list()
new_users = users.difference(self.existing_users)
self.existing_users = users
time.sleep(20) # wait until Delta is configured on the user side
for user in new_users:
for ci_prefix in ["ac1_", "ac2_", "ac3_", "ac4_", "ac5_", "ci-"]:
if user.startswith(ci_prefix):
continue
if user not in [c.addr for c in self.account.get_contacts()]:
print("Inviting", user)
contact = self.account.create_contact(user)
chat = contact.create_chat()
chat.send_text(
"Welcome to %s! Here you can try out Delta Chat." % (self.domain,)
)
chat.send_text(
"I prepared some webxdc apps for you, if you are interested:"
)
chat.send_file(pkg_resources.resource_filename(__name__, "editor.xdc"))
chat.send_file(
pkg_resources.resource_filename(__name__, "tower-builder.xdc")
)
chat.send_text(
"You can visit https://webxdc.org/apps to discover more apps! "
"Some of these games you can also play with friends, directly in the chat."
)
def main():
args = configargparse.ArgumentParser()
args.add_argument("--db_path", help="location of the Delta Chat database")
args.add_argument(
"--passdb", default=PASSDB_PATH, help="location of the chatmail passdb"
)
args.add_argument("--show-ffi", action="store_true", help="print Delta Chat log")
ops = args.parse_args()
# ensuring account data directory
if ops.db_path is None:
tempdir = tempfile.TemporaryDirectory(prefix="hellobot")
ops.db_path = tempdir.name
elif not os.path.exists(ops.db_path):
os.mkdir(ops.db_path)
ac = setup_account(ops.db_path, ops.show_ffi)
greeter = GreetBot(ops.passdb, ac)
print("waiting for new chatmail users...")
while 1:
greeter.greet_users()
sleep(5)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Chatmail greeterbot, a Delta Chat bot to greet new users
[Service]
ExecStart={execpath} --passdb {passdb_path} --db_path /home/vmail/greeterbot/ --show-ffi
User=vmail
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

Binary file not shown.

View File

@@ -101,11 +101,13 @@ def _install_remote_venv_with_chatmaild(config) -> None:
"doveauth",
"filtermail",
"echobot",
"greeterbot",
):
params = dict(
execpath=f"{remote_venv_dir}/bin/{fn}",
config_path=remote_chatmail_inipath,
remote_venv_dir=remote_venv_dir,
passdb_path="/home/vmail/passdb.sqlite",
)
source_path = importlib.resources.files("chatmaild").joinpath(f"{fn}.service.f")
content = source_path.read_text().format(**params).encode()

View File

@@ -6,7 +6,7 @@ import io
def gen_qr_png_data(maildomain):
url = f"DCACCOUNT:https://{maildomain}/new"
url = f"DCACCOUNT:https://{maildomain}/cgi-bin/newemail.py"
image = gen_qr(maildomain, url)
temp = io.BytesIO()
image.save(temp, format="png")

View File

@@ -45,22 +45,8 @@ http {
default_type text/plain;
}
location /new {
if ($request_method = GET) {
# Redirect to Delta Chat,
# which will in turn do a POST request.
return 301 dcaccount:https://{{ config.domain_name }}/new;
}
fastcgi_pass unix:/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py;
}
# Old URL for compatibility with e.g. printed QR codes.
location /cgi-bin/newemail.py {
return 301 /new;
}
# add cgi-bin support
include /usr/share/doc/fcgiwrap/examples/nginx.conf;
}
# Redirect www. to non-www

View File

@@ -1,60 +1,30 @@
rules {
## Reject on missing or invalid DKIM signatures.
##
## We require DKIM signature on incoming mails regardless of DMARC policy.
# R_DKIM_REJECT: DKIM reject inserted by `dkim` module.
REJECT_INVALID_DKIM {
REJECT_DKIM_SPF {
action = "reject";
expression = "R_DKIM_REJECT";
message = "Rejected due to invalid DKIM signature";
# Reject if
# - R_DKIM_RJECT: DKIM reject inserted by `dkim` module.
# - R_DKIM_PERMFAIL: permanent failure inserted by `dkim` module e.g. no DKIM DNS record found.
# - No DKIM signing (R_DKIM_NA symbol inserted by `dkim` module)
#
# - SPF failure (R_SPF_FAIL)
# - SPF permanent failure, e.g. failed to resolve DNS record referenced from SPF (R_SPF_PERMFAIL)
#
# - DMARC policy failure (DMARC_POLICY_REJECT)
#
# Do not reject if:
# - R_DKIM_TEMPFAIL, it is a DNS resolution failure
# and we do not want to lose messages because of faulty network.
#
# - R_SPF_SOFTFAIL
# - R_SPF_NEUTRAL
# - R_SPF_DNSFAIL
# - R_SPF_NA
#
# - DMARC_DNSFAIL
# - DMARC_NA
# - DMARC_POLICY_SOFTFAIL
# - DMARC_POLICY_QUARANTINE
# - DMARC_BAD_POLICY
expression = "R_DKIM_REJECT | R_DKIM_PERMFAIL | R_DKIM_NA | R_SPF_FAIL | R_SPF_PERMFAIL | DMARC_POLICY_REJECT";
}
# R_DKIM_PERMFAIL: permanent failure inserted by `dkim` module e.g. no DKIM DNS record found.
REJECT_PERMFAIL_DKIM {
action = "reject";
expression = "R_DKIM_PERMFAIL";
message = "Rejected due to missing DKIM DNS entry";
}
# No DKIM signature (R_DKIM_NA symbol inserted by `dkim` module).
REJECT_MISSING_DKIM {
action = "reject";
expression = "R_DKIM_NA";
message = "Rejected due to missing DKIM signature";
}
## Reject on SPF failure.
# - SPF failure (R_SPF_FAIL)
# - SPF permanent failure, e.g. failed to resolve DNS record referenced from SPF (R_SPF_PERMFAIL)
REJECT_SPF {
action = "reject";
expression = "R_SPF_FAIL | R_SPF_PERMFAIL";
message = "Rejected due to failed SPF check";
}
# Reject on DMARC policy check failure.
REJECT_DMARC {
action = "reject";
expression = "DMARC_POLICY_REJECT";
message = "Rejected due to DMARC policy";
}
# Do not reject if:
# - R_DKIM_TEMPFAIL, it is a DNS resolution failure
# and we do not want to lose messages because of faulty network.
#
# - R_SPF_SOFTFAIL
# - R_SPF_NEUTRAL
# - R_SPF_DNSFAIL
# - R_SPF_NA
#
# - DMARC_DNSFAIL
# - DMARC_NA
# - DMARC_POLICY_SOFTFAIL
# - DMARC_POLICY_QUARANTINE
# - DMARC_BAD_POLICY
}

View File

@@ -7,7 +7,7 @@ Welcome to instant, interoperable and [privacy-preserving](privacy.html) messagi
👉 **Tap** or scan this QR code to get a random `@{{config.mail_domain}}` e-mail address
<a href="DCACCOUNT:https://{{ config.mail_domain }}/new">
<a href="DCACCOUNT:https://{{ config.mail_domain }}/cgi-bin/newemail.py">
<img width=300 style="float: none;" src="qr-chatmail-invite-{{config.mail_domain}}.png" /></a>
🐣 **Choose** your Avatar and Name