Compare commits
4 Commits
webform
...
121a02b8ae
Author | SHA1 | Date | |
---|---|---|---|
121a02b8ae | |||
e18f1c7a96 | |||
cc5ad89977 | |||
a1fd586dbd |
11
Makefile
11
Makefile
@ -1,8 +1,10 @@
|
|||||||
# Needs python3 >= 3.9, sed, git for build
|
# Needs python3 >= 3.9, sed, git for build, docker for tests
|
||||||
build: clean
|
build: clean
|
||||||
python3 -m pip install -r requirements.txt --no-compile --target build
|
python3 -m pip install -r requirements.txt --no-compile --target build
|
||||||
cp -r mail4one/ build/
|
cp -r mail4one/ build/
|
||||||
sed -i "s/DEVELOMENT/$(shell scripts/get_version.sh)/" build/mail4one/version.py
|
sed -i "s/DEVELOMENT/$(shell scripts/get_version.sh)/" build/mail4one/version.py
|
||||||
|
find build -name "*.pyi" -o -name "py.typed" | xargs -I typefile rm typefile
|
||||||
|
rm -rf build/bin
|
||||||
rm -rf build/mail4one/__pycache__
|
rm -rf build/mail4one/__pycache__
|
||||||
rm -rf build/*.dist-info
|
rm -rf build/*.dist-info
|
||||||
python3 -m zipapp \
|
python3 -m zipapp \
|
||||||
@ -18,11 +20,13 @@ clean:
|
|||||||
docker-tests:
|
docker-tests:
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.11-alpine sh scripts/runtests.sh
|
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.11-alpine sh scripts/runtests.sh
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.10-alpine sh scripts/runtests.sh
|
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.10-alpine sh scripts/runtests.sh
|
||||||
|
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.12 sh scripts/runtests.sh
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.11 sh scripts/runtests.sh
|
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.11 sh scripts/runtests.sh
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.10 sh scripts/runtests.sh
|
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.10 sh scripts/runtests.sh
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.9 sh scripts/runtests.sh
|
docker run --pull=always -v `pwd`:/app -w /app --rm python:3.9 sh scripts/runtests.sh
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
# Below targets for devs. Need pipenv, black installed
|
||||||
|
|
||||||
requirements.txt: Pipfile.lock
|
requirements.txt: Pipfile.lock
|
||||||
pipenv requirements > requirements.txt
|
pipenv requirements > requirements.txt
|
||||||
@ -38,6 +42,11 @@ setup:
|
|||||||
cleanup:
|
cleanup:
|
||||||
pipenv --rm
|
pipenv --rm
|
||||||
|
|
||||||
|
update:
|
||||||
|
rm requirements.txt Pipfile.lock
|
||||||
|
pipenv update
|
||||||
|
pipenv requirements > requirements.txt
|
||||||
|
|
||||||
shell:
|
shell:
|
||||||
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
|
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
|
||||||
|
|
||||||
|
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"
|
||||||
|
18
Pipfile.lock
generated
18
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "6b224d3d72187a39e4fba357ffb4b162dc28994bd869c7960fcfd5acbb56c959"
|
"sha256": "5c16e20a67c73101d465516d657e21c6c1d3f853ae16dcfe782b7f9e8ba139e5"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -22,16 +22,9 @@
|
|||||||
"sha256:f9243b7dfe00aaf567da8728d891752426b51392174a34d2cf5c18053b63dcbc"
|
"sha256:f9243b7dfe00aaf567da8728d891752426b51392174a34d2cf5c18053b63dcbc"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
"markers": "python_version ~= '3.7'",
|
||||||
"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",
|
||||||
@ -42,11 +35,11 @@
|
|||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
|
"sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
|
||||||
"sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
|
"sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==23.1.0"
|
"version": "==23.2.0"
|
||||||
},
|
},
|
||||||
"python-jata": {
|
"python-jata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -54,6 +47,7 @@
|
|||||||
"sha256:ff4cd7ca75c9a8306b69ef6e878c296a5602f3279c6f9a82b6105b8eba764760"
|
"sha256:ff4cd7ca75c9a8306b69ef6e878c296a5602f3279c6f9a82b6105b8eba764760"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.2"
|
"version": "==1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Mail4one
|
# Mail4one
|
||||||
|
|
||||||
Personal mail server for a single user or a small family. Written in pure python with minimal dependencies.
|
Personal mail server for a single user or a small family. Written in pure python with [minimal dependencies](Pipfile).
|
||||||
Designed for dynamic alias based workflow where a different alias is used for each purpose.
|
Designed for dynamic alias based workflow where a different alias is used for each purpose.
|
||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
@ -23,7 +23,7 @@ Mail4one only takes care of receiving and serving email. For sending email, use
|
|||||||
|
|
||||||
Most of them have generous free tier which is more than enough for personal use.
|
Most of them have generous free tier which is more than enough for personal use.
|
||||||
|
|
||||||
Sending email is tricky. Even if everything is correctly setup (DMARC, DKIM, SPF), popular email vendors like google, microsoft may mark emails sent from your IP as spam for no reason.
|
Sending email is tricky. Even if everything is correctly setup (DMARC, DKIM, SPF), popular email vendors like google, microsoft may mark emails sent from your IP as spam for no reason. Hence using a dedicated service is the only reliable way to send emails.
|
||||||
|
|
||||||
# Community
|
# Community
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ This should generate `mail4one.pyz` in current folder. This is a [executable pyt
|
|||||||
* Write dedicated documentation
|
* Write dedicated documentation
|
||||||
* Test with more email clients ([Thunderbird](https://www.thunderbird.net/) and [k9mail](https://k9mail.app/) are tested now)
|
* Test with more email clients ([Thunderbird](https://www.thunderbird.net/) and [k9mail](https://k9mail.app/) are tested now)
|
||||||
* IMAP support
|
* IMAP support
|
||||||
* Web UI for editing config
|
* Web UI for editing config ([WIP](https://github.com/mail4one/mail4one/tree/webform))
|
||||||
* Support email submission from client to forward to other senders or direct delivery
|
* Support email submission from client to forward to other senders or direct delivery
|
||||||
* Optional SPAM filtering
|
* Optional SPAM filtering
|
||||||
* Optional DMARC,SPF,DKIM verification
|
* Optional DMARC,SPF,DKIM verification
|
||||||
|
@ -107,7 +107,7 @@ cd test_dir
|
|||||||
python3 -m http.server 25
|
python3 -m http.server 25
|
||||||
```
|
```
|
||||||
In local machine or a browser
|
In local machine or a browser
|
||||||
You should see file listing a, b, c. Repeat for port 465, 995 to make sure firewall rules and dns is working
|
You should see file listing a, b, c. Repeat for port 465, 995 to make sure firewall rules and dns is working
|
||||||
```sh
|
```sh
|
||||||
curl http://mail.example.com:25
|
curl http://mail.example.com:25
|
||||||
```
|
```
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
# certbot deploy hook to copy certificates to mail4one when renewed.
|
# certbot deploy hook to copy certificates to mail4one when renewed.
|
||||||
# Initial setup, Install certbot(https://certbot.eff.org/) and run `certbot certonly` as root
|
# Initial setup, Install certbot(https://certbot.eff.org/) and run `certbot certonly` as root
|
||||||
|
# Doc: https://eff-certbot.readthedocs.io/en/latest/using.html#renewing-certificates
|
||||||
#
|
#
|
||||||
# This file is supposed to be copied to /etc/letsencrypt/renewal-hooks/deploy/
|
# This file is supposed to be copied to /etc/letsencrypt/renewal-hooks/deploy/
|
||||||
# Change the mail domain to the one on MX record
|
# Change the mail domain to the one on MX record
|
||||||
|
|
||||||
set -x
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$RENEWED_DOMAINS" = "mail.mydomain.com" ]
|
if [ "$RENEWED_DOMAINS" = "mail.mydomain.com" ]
|
||||||
then
|
then
|
||||||
mkdir -p /var/lib/mail4one/certs
|
mkdir -p /var/lib/mail4one/certs
|
||||||
@ -17,4 +15,5 @@ then
|
|||||||
cp "$RENEWED_LINEAGE/fullchain.pem" /var/lib/mail4one/certs/
|
cp "$RENEWED_LINEAGE/fullchain.pem" /var/lib/mail4one/certs/
|
||||||
cp "$RENEWED_LINEAGE/privkey.pem" /var/lib/mail4one/certs/
|
cp "$RENEWED_LINEAGE/privkey.pem" /var/lib/mail4one/certs/
|
||||||
systemctl restart mail4one.service
|
systemctl restart mail4one.service
|
||||||
|
echo "$(date) Renewed and deployed certificates for mail4one" >> /var/log/mail4one-cert-renew.log
|
||||||
fi
|
fi
|
||||||
|
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"
|
|
@ -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
|
||||||
@ -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=}")
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
)
|
|
@ -1,5 +1,5 @@
|
|||||||
-i https://pypi.org/simple
|
-i https://pypi.org/simple
|
||||||
aiosmtpd==1.4.4.post2
|
aiosmtpd==1.4.4.post2; python_version ~= '3.7'
|
||||||
atpublic==4.0 ; python_version >= '3.8'
|
atpublic==4.0; python_version >= '3.8'
|
||||||
attrs==23.1.0 ; python_version >= '3.7'
|
attrs==23.2.0; python_version >= '3.7'
|
||||||
python-jata==1.2
|
python-jata==1.2; python_version >= '3.8'
|
||||||
|
@ -8,7 +8,7 @@ then
|
|||||||
tag_val=$(git describe --dirty=DIRTY --exact-match)
|
tag_val=$(git describe --dirty=DIRTY --exact-match)
|
||||||
case "$tag_val" in
|
case "$tag_val" in
|
||||||
*DIRTY)
|
*DIRTY)
|
||||||
echo "git=$commit-changes"
|
echo "git-$commit-changes"
|
||||||
;;
|
;;
|
||||||
v*) # Only consider tags starting with v
|
v*) # Only consider tags starting with v
|
||||||
echo "$tag_val"
|
echo "$tag_val"
|
||||||
|
Reference in New Issue
Block a user