diff --git a/chatmaild/pyproject.toml b/chatmaild/pyproject.toml index 5e74f5c7..473c406e 100644 --- a/chatmaild/pyproject.toml +++ b/chatmaild/pyproject.toml @@ -6,7 +6,8 @@ build-backend = "setuptools.build_meta" name = "chatmaild" version = "0.1" dependencies = [ - "aiosmtpd" + "aiosmtpd", + "flask", ] [project.scripts] diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index 95a00d41..11a2c4d4 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -3,7 +3,6 @@ import os import time import sys import json -import crypt from socketserver import ( UnixStreamServer, StreamRequestHandler, @@ -12,39 +11,7 @@ from socketserver import ( import pwd from .database import Database - -NOCREATE_FILE = "/etc/chatmail-nocreate" - - -def encrypt_password(password: str): - # https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ - passhash = crypt.crypt(password, crypt.METHOD_SHA512) - return "{SHA512-CRYPT}" + passhash - - -def is_allowed_to_create(user, cleartext_password) -> bool: - """Return True if user and password are admissable.""" - if os.path.exists(NOCREATE_FILE): - logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") - return False - - if len(cleartext_password) < 10: - logging.warning("Password needs to be at least 10 characters long") - return False - - parts = user.split("@") - if len(parts) != 2: - logging.warning(f"user {user!r} is not a proper e-mail address") - return False - localpart, domain = parts - - if domain == "nine.testrun.org": - # nine.testrun.org policy, username has to be exactly nine chars - if len(localpart) != 9: - logging.warning(f"localpart {localpart!r} has not exactly nine chars") - return False - - return True +from .util import is_allowed_to_create, encrypt_password, get_mail_domain def get_user_data(db, user): @@ -123,8 +90,7 @@ def main(): socket = sys.argv[1] passwd_entry = pwd.getpwnam(sys.argv[2]) db = Database(sys.argv[3]) - with open("/etc/mailname", "r") as fp: - mail_domain = fp.read().strip() + mail_domain = get_mail_domain() class Handler(StreamRequestHandler): def handle(self): diff --git a/chatmaild/src/chatmaild/util.py b/chatmaild/src/chatmaild/util.py new file mode 100644 index 00000000..30588107 --- /dev/null +++ b/chatmaild/src/chatmaild/util.py @@ -0,0 +1,56 @@ +import base64 +import random +import logging +import os +import crypt + + +NOCREATE_FILE = "/etc/chatmail-nocreate" + + +def is_allowed_to_create(user, cleartext_password) -> bool: + """Return True if user and password are admissable.""" + if os.path.exists(NOCREATE_FILE): + logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") + return False + + if len(cleartext_password) < 10: + logging.warning("Password needs to be at least 10 characters long") + return False + + parts = user.split("@") + if len(parts) != 2: + logging.warning(f"user {user!r} is not a proper e-mail address") + return False + localpart, domain = parts + + if domain == "nine.testrun.org": + # nine.testrun.org policy, username has to be exactly nine chars + if len(localpart) != 9: + logging.warning(f"localpart {localpart!r} has not exactly nine chars") + return False + + return True + + +def gen_password(): + with open("/dev/urandom", "rb") as f: + s = f.read(21) + return base64.b64encode(s).decode("ascii")[:12] + + +def encrypt_password(password: str): + # https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ + passhash = crypt.crypt(password, crypt.METHOD_SHA512) + return "{SHA512-CRYPT}" + passhash + + +def get_mail_domain(): + with open("/etc/mailname", "r") as fp: + return fp.read().strip() + + +def get_valid_email_addr(length=9, chars="2345789acdefghjkmnpqrstuvwxyz"): + localpart = "".join(random.choice(chars) for i in range(length)) + mail_domain = get_mail_domain() + return f"{localpart}@{mail_domain}" diff --git a/chatmaild/src/chatmaild/web.py b/chatmaild/src/chatmaild/web.py new file mode 100644 index 00000000..e4ae3bd2 --- /dev/null +++ b/chatmaild/src/chatmaild/web.py @@ -0,0 +1,39 @@ +from flask import Flask, jsonify, request +import time + +from database import Database +from util import gen_password, get_valid_email_addr, encrypt_password +from doveauth import get_user_data + + +def create_app_from_db_path(db_path=None): + db = Database(db_path) + return create_app_from_db(db) + + +def create_app_from_db(db): + app = Flask("chatmaild-http") + app.db = db + + @app.route("/", methods=["POST"]) + def new_email(): + for i in range(10): + addr = get_valid_email_addr() + if not get_user_data(db, addr): + cleartext_password = gen_password() + encrypted_password = encrypt_password(cleartext_password) + q = """INSERT INTO users (addr, password, last_login) + VALUES (?, ?, ?)""" + with db.write_transaction() as conn: + conn.execute(q, (addr, encrypted_password, int(time.time()))) + return jsonify( + email=addr, + password=cleartext_password, + ) + return jsonify( + type="error", + status_code=409, + reason="all 10 email addresses we tried are taken" + ) + + return app