Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
2da8f13ebd |
1
Pipfile
1
Pipfile
@ -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
10
Pipfile.lock
generated
@ -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
1
jsdev/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
tmp
|
|
@ -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
|
|
473
jsdev/index.html
473
jsdev/index.html
@ -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>
|
|
@ -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"
|
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
@ -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=}")
|
||||||
|
|
||||||
|
@ -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=}"
|
||||||
)
|
)
|
||||||
|
@ -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>
|
|
@ -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,
|
|
||||||
)
|
|
Reference in New Issue
Block a user