1 Commits

Author SHA1 Message Date
2da8f13ebd typehint changes for python 3.8 2023-06-30 17:42:39 -04:00
14 changed files with 32 additions and 748 deletions

View File

@ -8,7 +8,6 @@ name = "pypi"
[packages] [packages]
aiosmtpd = "*" aiosmtpd = "*"
python-jata = "*" python-jata = "*"
asyncbasehttp = "*"
[requires] [requires]
python_version = "3" python_version = "3"

10
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "6b224d3d72187a39e4fba357ffb4b162dc28994bd869c7960fcfd5acbb56c959" "sha256": "5c16e20a67c73101d465516d657e21c6c1d3f853ae16dcfe782b7f9e8ba139e5"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -24,14 +24,6 @@
"index": "pypi", "index": "pypi",
"version": "==1.4.4.post2" "version": "==1.4.4.post2"
}, },
"asyncbasehttp": {
"hashes": [
"sha256:2cea7b61113bb1e69d1c90f32919f004e7f56a7e1e2955e8a6286ce40f9da188",
"sha256:70fb983c4df630a9b63081cb8ef6d2e06beb51c89f42a88da6c9a9c7ebf9fb89"
],
"index": "pypi",
"version": "==1.0"
},
"atpublic": { "atpublic": {
"hashes": [ "hashes": [
"sha256:0f40433219e124edf115c6c363808ca6f0e1cfa7d160d86b2fb94793086d1294", "sha256:0f40433219e124edf115c6c363808ca6f0e1cfa7d160d86b2fb94793086d1294",

1
jsdev/.gitignore vendored
View File

@ -1 +0,0 @@
tmp

View File

@ -1,44 +0,0 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "cp ./restart.sh ./tmp/main"
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@ -1,473 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<title>Mail4one Web config</title>
<script type="application/json" id="m41config">
{
"matches": [
{
"name": "mydomain",
"addr_rexs": [
".*@mydomain.com",
".*@m.mydomain.com"
]
},
{
"name": "personal",
"addrs": [
"first.last@mydomain.com",
"secret.name@mydomain.com"
]
}
],
"boxes": [
{
"name": "spam",
"rules": [
{
"match_name": "mydomain",
"negate": true,
"stop_check": true
}
]
},
{
"name": "important",
"rules": [
{
"match_name": "personal"
}
]
},
{
"name": "all",
"rules": [
{
"match_name": "default_match_all"
}
]
}
],
"users": [
{
"username": "mymobile",
"password_hash": "AFTY5EVN7AX47ZL7UMH3BETYWFBTAV3XHR73CEFAJBPN2NIHPWDZHV2UQSMSPHSQQ2A2BFQBNC77VL7F2UKATQNJZGYLCSU6C43UQDAQXWXSWNGAEPGIMG2F3QDKBXL3MRHY6K2BPID64ZR6LABLPVSF",
"mbox": "important"
},
{
"username": "mydesk",
"password_hash": "AFTY5EVN7AX47ZL7UMH3BETYWFBTAV3XHR73CEFAJBPN2NIHPWDZHV2UQSMSPHSQQ2A2BFQBNC77VL7F2UKATQNJZGYLCSU6C43UQDAQXWXSWNGAEPGIMG2F3QDKBXL3MRHY6K2BPID64ZR6LABLPVSF",
"mbox": "all"
}
]
}
</script>
<style>
td {
padding: 5px;
}
table {
border: 2px solid;
}
thead {
background-color: orange;
}
h1.page-title {
background-color: orange;
}
h3 {
text-align: center;
}
.outer {
display: flex;
justify-content: center;
background: grey;
}
.inner {
display: flex;
flex-direction: column;
align-items: center;
border: 2px solid;
background-color: lightyellow;
padding: 10px;
justify-content: space-around;
margin: 10px;
width: 1200px;
}
.m41-box {
display: grid;
grid-template-columns: 1fr 3fr;
margin: 10px;
// background-color: lightblue;
}
.multiline {
text-align: left;
padding: 5px;
padding-left: 15px;
}
#web-cfg-matches {
width: 90%;
text-align: center;
}
#web-cfg-matches tbody tr:nth-of-type(odd) {
background: lightblue;
}
#web-cfg-matches tbody tr:nth-of-type(even) {
background: lightgrey;
}
#web-cfg-boxes {
text-align: center;
width: 90%;
// display: grid;
// grid-template-columns: 1fr ;
//justify-content: space-around;
// justify-content: flex-start;
// background-color: blue;
}
#web-cfg-boxes tbody:nth-of-type(odd) {
background: lightblue;
}
#web-cfg-boxes tbody:nth-of-type(even) {
background: lightgrey;
}
#web-cfg-boxes tbody tr td div.dummy{
display: none;
}
#web-cfg-boxes tbody tr td div button.dummy {
display: none;
}
#web-cfg-boxes tbody:first-of-type tr td.box div button.up.dummy {
display: block;
}
#web-cfg-boxes tbody:first-of-type tr td.box div button.up.real {
display: none;
}
#web-cfg-boxes tbody:last-of-type tr td.box div button.down.dummy {
display: block;
}
#web-cfg-boxes tbody:last-of-type tr td.box div button.down.real {
display: none;
}
#web-cfg-boxes tbody:only-of-type tr td.box div.real {
display: none;
}
#web-cfg-boxes tbody:only-of-type tr td.box div.dummy {
display: flex;
}
td.box {
display: flex;
justify-content: center;
}
.button-group {
display: flex;
justify-content: center;
}
</style>
<script type="application/javascript">
"use strict"
// Globals
let server_config
let matches_table
let match_row_template
let boxes_table
let box_template
// let rule_template
function initGlobals() {
server_config = JSON.parse(document.getElementById('m41config').text)
matches_table = document.getElementById("web-cfg-matches")
match_row_template = document.getElementById("web-cfg-matches-row")
boxes_table = document.getElementById("web-cfg-boxes")
box_template = document.getElementById("web-cfg-box")
// rule_template = document.getElementById("web-cfg-boxes-rule-li")
}
function populate_match_table(matches_config) {
for (const { name: match_name, addrs, addr_rexs } of matches_config) {
const [match_type, match_values] = (() => {
if (addrs != undefined) {
return ["addrs", addrs]
} else {
return ["addr_rexs", addr_rexs]
}
})()
addMatchRow()
const last_row = matches_table.tBodies[0].lastElementChild
const [ ,name_cell, {firstElementChild: type_select}, value_cell ] = last_row.cells
name_cell.innerText = match_name
type_select.value = match_type
value_cell.innerText = match_values.join("\n")
}
}
function extract_match_table() {
let matches = []
for (let row of matches_table.tBodies[0].rows) {
const [ ,name_cell, {firstElementChild: type_select}, value_cell ] = row.cells
let m = {"name" : name_cell.innerText}
switch (type_select.value) {
case "addrs":
m["addrs"] = value_cell.innerText.split("\n")
break
case "addr_rexs":
m["addr_rexs"] = value_cell.innerText.split("\n")
break
}
matches.push(m)
}
return matches
}
function addMatchRow() {
let row_clone = match_row_template.content.cloneNode(true)
matches_table.tBodies[0].appendChild(row_clone)
}
function populate_boxes_list(boxes_config) {
for (const {name:box_name, rules} of boxes_config) {
addBox()
const tbody = boxes_table.lastElementChild
const box = tbody.firstElementChild
// console.log(box)
const [,
{children: [box_text, ]},
,
{firstElementChild: match_select},
{firstElementChild: negate_check},
{firstElementChild: stop_check},
] = box.children
box_text.value = box_name
const [first_rule, ...rest] = rules
const {match_name, negate = false, stop = false} = first_rule
match_select.value = match_name
negate_check.checked = negate
stop_check.checked = stop
for (const {match_name, negate = false, stop_check = false} of rest ) {
addRule(box)
const rule = tbody.lastElementChild
const [,
{firstElementChild: match_select},
{firstElementChild: negateCheck},
{firstElementChild: stopCheck}
] = rule.children
match_select.value = match_name
negate_check.checked = negate
stop_check.checked = stop
}
}
}
function addRule(box) {
box.parentElement.appendChild(box.cloneNode(true))
box.children[0].rowSpan++;
box.children[1].rowSpan++;
const newrule = box.parentElement.lastElementChild
newrule.removeClass("fist-tr")
newrule.firstElementChild.remove()
newrule.firstElementChild.remove()
}
function addBox() {
let box_clone = box_template.content.cloneNode(true)
boxes_table.appendChild(box_clone)
}
function moveUp(button) {
const li = button.parentElement.parentElement.parentElement
if (li.previousElementSibling != null) {
li.parentNode.insertBefore(li, li.previousElementSibling)
}
}
function moveDown(button) {
const li = button.parentElement.parentElement.parentElement
if (li.nextElementSibling != null) {
li.parentNode.insertBefore(li.nextElementSibling, li)
}
}
function main() {
initGlobals()
populate_match_table(server_config["matches"])
save()
document.getElementById("before").innerText = JSON.stringify(server_config["matches"], null, 2)
populate_boxes_list(server_config["boxes"])
}
function save() {
const matches = extract_match_table()
document.getElementById("after").innerText = JSON.stringify(matches, null, 2)
}
</script>
</head>
<body onload="main()">
<template id="web-cfg-matches-row">
<tr>
<td><button onClick="this.parentElement.parentElement.remove()"></button></td>
<td contentEditable></td>
<td>
<select>
<option value="addrs">List of addresses</option>
<option value="addr_rexs">List of regexes for addresses</option>
</select>
</td>
<td contentEditable class="multiline"></td>
</tr>
</template>
<template id="web-cfg-box">
<tbody>
<tr>
<td class="box">
<div class="button-group dummy">
<button disabled></button>
<button disabled></button>
<button disabled></button>
</div>
<div class="button-group real">
<button onClick="this.parentElement.parentElement.parentElement.parentElement.remove()"></button>
<button onClick="moveBoxUp(this)" class="real up"></button>
<button disabled class="dummy up"></button>
<button onClick="moveBoxDown(this)" class="real down"></button>
<button disabled class="dummy down"></button>
</div>
<button onClick="addRule(this.parentElement.parentElement)">+</button>
</td>
<td contentEditable>
<input type="text" contentEditable>
</td>
<td class="rule">
<div class="button-group dummy">
<button disabled></button>
<button disabled></button>
<button disabled></button>
</div>
<div class="button-group real">
<button onClick="this.parentElement.parentElement.parentElement.remove()"></button>
<button onClick="moveRuleUp(this)" class="real up"></button>
<button disabled class="dummy up"></button>
<button onClick="moveRuleDown(this)" class="real down"></button>
<button disabled class="dummy down"></button>
</div>
</td>
<td>
<select></select>
</td>
<td>
<input type=checkbox>
</td>
<td>
<input type=checkbox>
</td>
</tr>
</tbody>
</template>
<div class="outer">
<div class="inner">
<h1 class="page-title">Mail4one Web config</h1>
<div id="top-menu">
<button onClick="save()">Previous</button>
<button onClick="save()">Matches</button>
<button onClick="save()">Boxes</button>
<button onClick="save()">Users</button>
<button onClick="save()">JSON</button>
<button onClick="save()">Next</button>
</div>
<div id="match-page">
<h3> Matches </h3>
<table id="web-cfg-matches">
<thead>
<tr>
<th>
<button onClick="addMatchRow()">+</button>
</th>
<th>Match</th>
<th>Type</th>
<th>Values</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div id=box-page">
<h3>Boxes</h3>
<table id="web-cfg-boxes">
<thead>
<tr>
<th><button onClick="addBox()">+</button></th>
<th>Mailbox</th>
<th></th>
<th>Match</th>
<th>Invert</th>
<th>Stop</th>
</tr>
</thead>
</table>
</div>
<hr>
<h3>Before</h3>
<pre id="before"></pre>
<hr>
<h3>After</h3>
<pre id="after"></pre>
<hr>
</div>
</div>
</body>
</html>

View File

@ -1,11 +0,0 @@
#!/bin/sh
# kill "$(cat current.pid)"
# echo "starting python server"
python3 -m http.server
# echo $! > current.pid
# echo "started python server"

View File

@ -1,14 +1,14 @@
import json import json
import re import re
import logging import logging
from typing import Callable, Union, Optional from typing import Callable, Union, Optional, List, Tuple
from jata import Jata, MutableDefault from jata import Jata, MutableDefault
class Match(Jata): class Match(Jata):
name: str name: str
addrs: list[str] = MutableDefault(lambda: []) # type: ignore addrs: List[str] = MutableDefault(lambda: []) # type: ignore
addr_rexs: list[str] = MutableDefault(lambda: []) # type: ignore addr_rexs: List[str] = MutableDefault(lambda: []) # type: ignore
DEFAULT_MATCH_ALL = "default_match_all" DEFAULT_MATCH_ALL = "default_match_all"
@ -23,7 +23,7 @@ class Rule(Jata):
class Mbox(Jata): class Mbox(Jata):
name: str name: str
rules: list[Rule] rules: List[Rule]
DEFAULT_NULL_MBOX = "default_null_mbox" DEFAULT_NULL_MBOX = "default_null_mbox"
@ -77,18 +77,18 @@ class Config(Jata):
logging: Optional[LogCfg] = None logging: Optional[LogCfg] = None
mails_path: str mails_path: str
matches: list[Match] matches: List[Match]
boxes: list[Mbox] boxes: List[Mbox]
users: list[User] users: List[User]
servers: list[ServerCfg] servers: List[ServerCfg]
CheckerFn = Callable[[str], bool] CheckerFn = Callable[[str], bool]
Checker = tuple[str, CheckerFn, bool] Checker = Tuple[str, CheckerFn, bool]
def parse_checkers(cfg: Config) -> list[Checker]: def parse_checkers(cfg: Config) -> List[Checker]:
def make_match_fn(m: Match): def make_match_fn(m: Match):
if m.addrs and m.addr_rexs: if m.addrs and m.addr_rexs:
raise Exception("Both addrs and addr_rexs is set") raise Exception("Both addrs and addr_rexs is set")
@ -118,7 +118,7 @@ def parse_checkers(cfg: Config) -> list[Checker]:
] ]
def get_mboxes(addr: str, checks: list[Checker]) -> list[str]: def get_mboxes(addr: str, checks: List[Checker]) -> List[str]:
def inner(): def inner():
for mbox, match_fn, stop_check in checks: for mbox, match_fn, stop_check in checks:
if match_fn(addr): if match_fn(addr):
@ -130,7 +130,7 @@ def get_mboxes(addr: str, checks: list[Checker]) -> list[str]:
return list(inner()) return list(inner())
def gen_addr_to_mboxes(cfg: Config) -> Callable[[str], list[str]]: def gen_addr_to_mboxes(cfg: Config) -> Callable[[str], List[str]]:
checks = parse_checkers(cfg) checks = parse_checkers(cfg)
logging.info(f"Parsed checkers from config, {len(checks)=}") logging.info(f"Parsed checkers from config, {len(checks)=}")
return lambda addr: get_mboxes(addr, checks) return lambda addr: get_mboxes(addr, checks)

View File

@ -13,7 +13,7 @@ from .pwhash import parse_hash, check_pass, PWInfo
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
import random import random
from typing import Optional from typing import Optional, List, Tuple, Dict
from .poputils import ( from .poputils import (
InvalidCommand, InvalidCommand,
@ -46,7 +46,7 @@ class State:
class SharedState: class SharedState:
def __init__(self, mails_path: Path, users: dict[str, tuple[PWInfo, str]]): def __init__(self, mails_path: Path, users: dict[str, Tuple[PWInfo, str]]):
self.mails_path = mails_path self.mails_path = mails_path
self.users = users self.users = users
self.loggedin_users: set[str] = set() self.loggedin_users: set[str] = set()
@ -237,7 +237,7 @@ def trans_command_noop(_, __) -> None:
write(ok("Hmm")) write(ok("Hmm"))
async def process_transactions(mails_list: list[MailEntry]) -> set[str]: async def process_transactions(mails_list: List[MailEntry]) -> set[str]:
mails = MailList(mails_list) mails = MailList(mails_list)
def reset(_, __): def reset(_, __):
@ -328,7 +328,7 @@ async def start_session() -> None:
scfg().loggedin_users.remove(state().username) scfg().loggedin_users.remove(state().username)
def parse_users(users: list[User]) -> dict[str, tuple[PWInfo, str]]: def parse_users(users: List[User]) -> Dict[str, Tuple[PWInfo, str]]:
def inner(): def inner():
for user in users: for user in users:
user = User(user) user = User(user)
@ -338,7 +338,7 @@ def parse_users(users: list[User]) -> dict[str, tuple[PWInfo, str]]:
return dict(inner()) return dict(inner())
def make_pop_server_callback(mails_path: Path, users: list[User], timeout_seconds: int): def make_pop_server_callback(mails_path: Path, users: List[User], timeout_seconds: int):
scfg = SharedState(mails_path=mails_path, users=parse_users(users)) scfg = SharedState(mails_path=mails_path, users=parse_users(users))
async def session_cb(reader: StreamReader, writer: StreamWriter): async def session_cb(reader: StreamReader, writer: StreamWriter):
@ -362,7 +362,7 @@ async def create_pop_server(
host: str, host: str,
port: int, port: int,
mails_path: Path, mails_path: Path,
users: list[User], users: List[User],
ssl_context: Optional[ssl.SSLContext] = None, ssl_context: Optional[ssl.SSLContext] = None,
timeout_seconds: int = 60, timeout_seconds: int = 60,
) -> asyncio.Server: ) -> asyncio.Server:

View File

@ -2,6 +2,7 @@ import os
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from pathlib import Path from pathlib import Path
from typing import List
class ClientError(Exception): class ClientError(Exception):
@ -112,13 +113,13 @@ def files_in_path(path):
return [] return []
def get_mails_list(dirpath: Path) -> list[MailEntry]: def get_mails_list(dirpath: Path) -> List[MailEntry]:
files = files_in_path(dirpath) files = files_in_path(dirpath)
entries = [MailEntry(filename, path) for filename, path in files] entries = [MailEntry(filename, path) for filename, path in files]
return entries return entries
def set_nid(entries: list[MailEntry]): def set_nid(entries: List[MailEntry]):
entries.sort(reverse=True, key=lambda e: (e.c_time, e.uid)) entries.sort(reverse=True, key=lambda e: (e.c_time, e.uid))
for i, entry in enumerate(entries, start=1): for i, entry in enumerate(entries, start=1):
entry.nid = i entry.nid = i
@ -130,7 +131,7 @@ def get_mail(entry: MailEntry) -> bytes:
class MailList: class MailList:
def __init__(self, entries: list[MailEntry]): def __init__(self, entries: List[MailEntry]):
self.entries = entries self.entries = entries
set_nid(self.entries) set_nid(self.entries)
self.mails_map = {str(e.nid): e for e in entries} self.mails_map = {str(e.nid): e for e in entries}

View File

@ -16,8 +16,6 @@ VERSION = b"\x01"
SALT_LEN = 30 SALT_LEN = 30
KEY_LEN = 64 # This is python default KEY_LEN = 64 # This is python default
# len(VERSION) + SALT_LEN + KEY_LEN should be multiple of 5 to avoid base32 padding
def gen_pwhash(password: str) -> str: def gen_pwhash(password: str) -> str:
salt = os.urandom(SALT_LEN) salt = os.urandom(SALT_LEN)

View File

@ -10,7 +10,6 @@ from getpass import getpass
from .smtp import create_smtp_server_starttls, create_smtp_server from .smtp import create_smtp_server_starttls, create_smtp_server
from .pop3 import create_pop_server from .pop3 import create_pop_server
from .version import VERSION from .version import VERSION
from .web_config import create_web_config_server
from . import config from . import config
from . import pwhash from . import pwhash
@ -58,7 +57,7 @@ async def a_main(cfg: config.Config) -> None:
return host return host
mbox_finder = config.gen_addr_to_mboxes(cfg) mbox_finder = config.gen_addr_to_mboxes(cfg)
servers: list[asyncio.Server] = [] servers: List[asyncio.Server] = []
if not cfg.servers: if not cfg.servers:
logging.warning("Nothing to do!") logging.warning("Nothing to do!")
@ -99,14 +98,6 @@ async def a_main(cfg: config.Config) -> None:
ssl_context=get_tls_context(smtp.tls), ssl_context=get_tls_context(smtp.tls),
) )
servers.append(smtp_server) servers.append(smtp_server)
elif scfg.server_type == "web_config":
web = config.ServerCfg(scfg)
web_server = await create_web_config_server(
host=get_host(web.host),
port=web.port,
ssl_context=get_tls_context(web.tls),
)
servers.append(web_server)
else: else:
logging.error(f"Unknown server {scfg.server_type=}") logging.error(f"Unknown server {scfg.server_type=}")

View File

@ -7,7 +7,7 @@ import uuid
import shutil import shutil
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional, List
from . import config from . import config
from email.message import Message from email.message import Message
import email.policy import email.policy
@ -25,7 +25,7 @@ logger = logging.getLogger("smtp")
class MyHandler(AsyncMessage): class MyHandler(AsyncMessage):
def __init__(self, mails_path: Path, mbox_finder: Callable[[str], list[str]]): def __init__(self, mails_path: Path, mbox_finder: Callable[[str], List[str]]):
super().__init__() super().__init__()
self.mails_path = mails_path self.mails_path = mails_path
self.mbox_finder = mbox_finder self.mbox_finder = mbox_finder
@ -63,7 +63,7 @@ class MyHandler(AsyncMessage):
def protocol_factory_starttls( def protocol_factory_starttls(
mails_path: Path, mbox_finder: Callable[[str], list[str]], context: ssl.SSLContext mails_path: Path, mbox_finder: Callable[[str], List[str]], context: ssl.SSLContext
): ):
logger.info("Got smtp client cb starttls") logger.info("Got smtp client cb starttls")
try: try:
@ -80,7 +80,7 @@ def protocol_factory_starttls(
return smtp return smtp
def protocol_factory(mails_path: Path, mbox_finder: Callable[[str], list[str]]): def protocol_factory(mails_path: Path, mbox_finder: Callable[[str], List[str]]):
logger.info("Got smtp client cb") logger.info("Got smtp client cb")
try: try:
handler = MyHandler(mails_path, mbox_finder) handler = MyHandler(mails_path, mbox_finder)
@ -95,9 +95,9 @@ async def create_smtp_server_starttls(
host: str, host: str,
port: int, port: int,
mails_path: Path, mails_path: Path,
mbox_finder: Callable[[str], list[str]], mbox_finder: Callable[[str], List[str]],
ssl_context: ssl.SSLContext, ssl_context: ssl.SSLContext,
) -> asyncio.Server: ):
logging.info( logging.info(
f"Starting SMTP STARTTLS server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}" f"Starting SMTP STARTTLS server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
) )
@ -114,9 +114,9 @@ async def create_smtp_server(
host: str, host: str,
port: int, port: int,
mails_path: Path, mails_path: Path,
mbox_finder: Callable[[str], list[str]], mbox_finder: Callable[[str], List[str]],
ssl_context: Optional[ssl.SSLContext] = None, ssl_context: Optional[ssl.SSLContext] = None,
) -> asyncio.Server: ):
logging.info( logging.info(
f"Starting SMTP server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}" f"Starting SMTP server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
) )

View File

@ -1,88 +0,0 @@
<!doctype html>
<html>
<head>
<title>Mail4one Web config</title>
<script type="application/json" id="m41config">
{
"matches": [
{
"name": "mydomain",
"addr_rexs": [
".*@mydomain.com",
".*@m.mydomain.com"
]
},
{
"name": "personal",
"addrs": [
"first.last@mydomain.com",
"secret.name@mydomain.com"
]
}
],
"boxes": [
{
"name": "spam",
"rules": [
{
"match_name": "mydomain",
"negate": true,
"stop_check": true
}
]
},
{
"name": "important",
"rules": [
{
"match_name": "personal"
}
]
},
{
"name": "all",
"rules": [
{
"match_name": "default_match_all"
}
]
}
],
"users": [
{
"username": "mymobile",
"password_hash": "AFTY5EVN7AX47ZL7UMH3BETYWFBTAV3XHR73CEFAJBPN2NIHPWDZHV2UQSMSPHSQQ2A2BFQBNC77VL7F2UKATQNJZGYLCSU6C43UQDAQXWXSWNGAEPGIMG2F3QDKBXL3MRHY6K2BPID64ZR6LABLPVSF",
"mbox": "important"
},
{
"username": "mydesk",
"password_hash": "AFTY5EVN7AX47ZL7UMH3BETYWFBTAV3XHR73CEFAJBPN2NIHPWDZHV2UQSMSPHSQQ2A2BFQBNC77VL7F2UKATQNJZGYLCSU6C43UQDAQXWXSWNGAEPGIMG2F3QDKBXL3MRHY6K2BPID64ZR6LABLPVSF",
"mbox": "all"
}
]
}
</script>
<script type="application/javascript">
function main() {
document.write("hello world")
}
</script>
</head>
<body onload="main()">
<template id="web-cfg-matches-row">
<tr>
<td>Name</td>
<td>Type</td>
<td>Values</td>
</tr>
</template>
<h1>Mail4one Web config</h1>
<table id="web-cfg-matches">
<tr>
<th>Name</th>
<th>Type</th>
<th>Values</th>
</tr>
</table>
</body>
</html>

View File

@ -1,80 +0,0 @@
import asyncio
from asyncbasehttp import request_handler, Request, Response, RequestHandler
from typing import Optional, Tuple
import ssl
import logging
from pprint import pprint
import http
from base64 import b64decode
from .pwhash import gen_pwhash, parse_hash, PWInfo, check_pass
import pkgutil
def get_template() -> bytes:
if data:= pkgutil.get_data('mail4one', 'template_web_config.html'):
return data
raise Exception("Failed to get template data from 'template_web_config.html'")
def get_dummy_pwinfo() -> PWInfo:
pwhash = gen_pwhash("world")
return parse_hash(pwhash)
class WebonfigHandler(RequestHandler):
def __init__(self, username: str, pwinfo: PWInfo):
self.username = username.encode()
self.pwinfo = pwinfo
self.auth_required = True
def do_auth(self, req: Request) -> Tuple[bool, Optional[Response]]:
def resp_unauthorized():
resp = Response.no_body_response(http.HTTPStatus.UNAUTHORIZED)
resp.add_header("WWW-Authenticate", 'Basic realm="Mail4one"')
return resp
auth_header = req.headers["Authorization"]
if not auth_header:
return False, resp_unauthorized()
if not auth_header.startswith("Basic "):
logging.error("Authorization header malformed")
return False, Response.no_body_response(http.HTTPStatus.BAD_REQUEST)
userpassb64 = auth_header[len("Basic ") :]
try:
userpass = b64decode(userpassb64)
username, password = userpass.split(b":")
except:
logging.exception("bad request")
return False, Response.no_body_response(http.HTTPStatus.BAD_REQUEST)
if username == self.username and check_pass(password.decode(), self.pwinfo):
return True, None
return False, resp_unauthorized()
async def process_request(self, req: Request) -> Response:
if self.auth_required:
ok, resp = self.do_auth(req)
if not ok:
if resp:
return resp
else: # To silence mypy
raise Exception("Something went wrong!")
return Response.create_ok_response(get_template())
async def create_web_config_server(
host: str, port: int, ssl_context: Optional[ssl.SSLContext]
) -> asyncio.Server:
logging.info(f"template: {get_template().decode()}")
logging.info(f"Starting Webconfig server {host=}, {port=}, {ssl_context != None=}")
return await asyncio.start_server(
WebonfigHandler("hello", get_dummy_pwinfo()),
host=host,
port=port,
ssl=ssl_context,
)