Compare commits
1 Commits
121a02b8ae
...
py38
Author | SHA1 | Date | |
---|---|---|---|
2da8f13ebd |
11
Makefile
11
Makefile
@ -1,10 +1,8 @@
|
||||
# Needs python3 >= 3.9, sed, git for build, docker for tests
|
||||
# Needs python3 >= 3.9, sed, git for build
|
||||
build: clean
|
||||
python3 -m pip install -r requirements.txt --no-compile --target build
|
||||
cp -r mail4one/ build/
|
||||
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/*.dist-info
|
||||
python3 -m zipapp \
|
||||
@ -20,13 +18,11 @@ clean:
|
||||
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.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.10 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
|
||||
pipenv requirements > requirements.txt
|
||||
@ -42,11 +38,6 @@ setup:
|
||||
cleanup:
|
||||
pipenv --rm
|
||||
|
||||
update:
|
||||
rm requirements.txt Pipfile.lock
|
||||
pipenv update
|
||||
pipenv requirements > requirements.txt
|
||||
|
||||
shell:
|
||||
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
|
||||
|
||||
|
8
Pipfile.lock
generated
8
Pipfile.lock
generated
@ -22,7 +22,6 @@
|
||||
"sha256:f9243b7dfe00aaf567da8728d891752426b51392174a34d2cf5c18053b63dcbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version ~= '3.7'",
|
||||
"version": "==1.4.4.post2"
|
||||
},
|
||||
"atpublic": {
|
||||
@ -35,11 +34,11 @@
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30",
|
||||
"sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"
|
||||
"sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
|
||||
"sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.2.0"
|
||||
"version": "==23.1.0"
|
||||
},
|
||||
"python-jata": {
|
||||
"hashes": [
|
||||
@ -47,7 +46,6 @@
|
||||
"sha256:ff4cd7ca75c9a8306b69ef6e878c296a5602f3279c6f9a82b6105b8eba764760"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.2"
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Mail4one
|
||||
|
||||
Personal mail server for a single user or a small family. Written in pure python with [minimal dependencies](Pipfile).
|
||||
Personal mail server for a single user or a small family. Written in pure python with minimal dependencies.
|
||||
Designed for dynamic alias based workflow where a different alias is used for each purpose.
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
# Community
|
||||
|
||||
@ -62,7 +62,7 @@ This should generate `mail4one.pyz` in current folder. This is a [executable pyt
|
||||
* Write dedicated documentation
|
||||
* Test with more email clients ([Thunderbird](https://www.thunderbird.net/) and [k9mail](https://k9mail.app/) are tested now)
|
||||
* IMAP support
|
||||
* Web UI for editing config ([WIP](https://github.com/mail4one/mail4one/tree/webform))
|
||||
* Web UI for editing config
|
||||
* Support email submission from client to forward to other senders or direct delivery
|
||||
* Optional SPAM filtering
|
||||
* Optional DMARC,SPF,DKIM verification
|
||||
|
@ -107,7 +107,7 @@ cd test_dir
|
||||
python3 -m http.server 25
|
||||
```
|
||||
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
|
||||
curl http://mail.example.com:25
|
||||
```
|
||||
|
@ -2,11 +2,13 @@
|
||||
|
||||
# certbot deploy hook to copy certificates to mail4one when renewed.
|
||||
# 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/
|
||||
# Change the mail domain to the one on MX record
|
||||
|
||||
set -x
|
||||
|
||||
|
||||
if [ "$RENEWED_DOMAINS" = "mail.mydomain.com" ]
|
||||
then
|
||||
mkdir -p /var/lib/mail4one/certs
|
||||
@ -15,5 +17,4 @@ then
|
||||
cp "$RENEWED_LINEAGE/fullchain.pem" /var/lib/mail4one/certs/
|
||||
cp "$RENEWED_LINEAGE/privkey.pem" /var/lib/mail4one/certs/
|
||||
systemctl restart mail4one.service
|
||||
echo "$(date) Renewed and deployed certificates for mail4one" >> /var/log/mail4one-cert-renew.log
|
||||
fi
|
||||
|
@ -1,14 +1,14 @@
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from typing import Callable, Union, Optional
|
||||
from typing import Callable, Union, Optional, List, Tuple
|
||||
from jata import Jata, MutableDefault
|
||||
|
||||
|
||||
class Match(Jata):
|
||||
name: str
|
||||
addrs: list[str] = MutableDefault(lambda: []) # type: ignore
|
||||
addr_rexs: list[str] = MutableDefault(lambda: []) # type: ignore
|
||||
addrs: List[str] = MutableDefault(lambda: []) # type: ignore
|
||||
addr_rexs: List[str] = MutableDefault(lambda: []) # type: ignore
|
||||
|
||||
|
||||
DEFAULT_MATCH_ALL = "default_match_all"
|
||||
@ -23,7 +23,7 @@ class Rule(Jata):
|
||||
|
||||
class Mbox(Jata):
|
||||
name: str
|
||||
rules: list[Rule]
|
||||
rules: List[Rule]
|
||||
|
||||
|
||||
DEFAULT_NULL_MBOX = "default_null_mbox"
|
||||
@ -77,18 +77,18 @@ class Config(Jata):
|
||||
logging: Optional[LogCfg] = None
|
||||
|
||||
mails_path: str
|
||||
matches: list[Match]
|
||||
boxes: list[Mbox]
|
||||
users: list[User]
|
||||
matches: List[Match]
|
||||
boxes: List[Mbox]
|
||||
users: List[User]
|
||||
|
||||
servers: list[ServerCfg]
|
||||
servers: List[ServerCfg]
|
||||
|
||||
|
||||
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):
|
||||
if m.addrs and m.addr_rexs:
|
||||
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():
|
||||
for mbox, match_fn, stop_check in checks:
|
||||
if match_fn(addr):
|
||||
@ -130,7 +130,7 @@ def get_mboxes(addr: str, checks: list[Checker]) -> list[str]:
|
||||
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)
|
||||
logging.info(f"Parsed checkers from config, {len(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
|
||||
import random
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Tuple, Dict
|
||||
|
||||
from .poputils import (
|
||||
InvalidCommand,
|
||||
@ -46,7 +46,7 @@ class State:
|
||||
|
||||
|
||||
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.users = users
|
||||
self.loggedin_users: set[str] = set()
|
||||
@ -237,7 +237,7 @@ def trans_command_noop(_, __) -> None:
|
||||
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)
|
||||
|
||||
def reset(_, __):
|
||||
@ -328,7 +328,7 @@ async def start_session() -> None:
|
||||
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():
|
||||
for user in users:
|
||||
user = User(user)
|
||||
@ -338,7 +338,7 @@ def parse_users(users: list[User]) -> dict[str, tuple[PWInfo, str]]:
|
||||
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))
|
||||
|
||||
async def session_cb(reader: StreamReader, writer: StreamWriter):
|
||||
@ -362,7 +362,7 @@ async def create_pop_server(
|
||||
host: str,
|
||||
port: int,
|
||||
mails_path: Path,
|
||||
users: list[User],
|
||||
users: List[User],
|
||||
ssl_context: Optional[ssl.SSLContext] = None,
|
||||
timeout_seconds: int = 60,
|
||||
) -> asyncio.Server:
|
||||
|
@ -2,6 +2,7 @@ import os
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
class ClientError(Exception):
|
||||
@ -112,13 +113,13 @@ def files_in_path(path):
|
||||
return []
|
||||
|
||||
|
||||
def get_mails_list(dirpath: Path) -> list[MailEntry]:
|
||||
def get_mails_list(dirpath: Path) -> List[MailEntry]:
|
||||
files = files_in_path(dirpath)
|
||||
entries = [MailEntry(filename, path) for filename, path in files]
|
||||
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))
|
||||
for i, entry in enumerate(entries, start=1):
|
||||
entry.nid = i
|
||||
@ -130,7 +131,7 @@ def get_mail(entry: MailEntry) -> bytes:
|
||||
|
||||
|
||||
class MailList:
|
||||
def __init__(self, entries: list[MailEntry]):
|
||||
def __init__(self, entries: List[MailEntry]):
|
||||
self.entries = entries
|
||||
set_nid(self.entries)
|
||||
self.mails_map = {str(e.nid): e for e in entries}
|
||||
|
@ -57,7 +57,7 @@ async def a_main(cfg: config.Config) -> None:
|
||||
return host
|
||||
|
||||
mbox_finder = config.gen_addr_to_mboxes(cfg)
|
||||
servers: list[asyncio.Server] = []
|
||||
servers: List[asyncio.Server] = []
|
||||
|
||||
if not cfg.servers:
|
||||
logging.warning("Nothing to do!")
|
||||
|
@ -7,7 +7,7 @@ import uuid
|
||||
import shutil
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional, List
|
||||
from . import config
|
||||
from email.message import Message
|
||||
import email.policy
|
||||
@ -25,7 +25,7 @@ logger = logging.getLogger("smtp")
|
||||
|
||||
|
||||
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__()
|
||||
self.mails_path = mails_path
|
||||
self.mbox_finder = mbox_finder
|
||||
@ -63,7 +63,7 @@ class MyHandler(AsyncMessage):
|
||||
|
||||
|
||||
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")
|
||||
try:
|
||||
@ -80,7 +80,7 @@ def protocol_factory_starttls(
|
||||
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")
|
||||
try:
|
||||
handler = MyHandler(mails_path, mbox_finder)
|
||||
@ -95,9 +95,9 @@ async def create_smtp_server_starttls(
|
||||
host: str,
|
||||
port: int,
|
||||
mails_path: Path,
|
||||
mbox_finder: Callable[[str], list[str]],
|
||||
mbox_finder: Callable[[str], List[str]],
|
||||
ssl_context: ssl.SSLContext,
|
||||
) -> asyncio.Server:
|
||||
):
|
||||
logging.info(
|
||||
f"Starting SMTP STARTTLS server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
|
||||
)
|
||||
@ -114,9 +114,9 @@ async def create_smtp_server(
|
||||
host: str,
|
||||
port: int,
|
||||
mails_path: Path,
|
||||
mbox_finder: Callable[[str], list[str]],
|
||||
mbox_finder: Callable[[str], List[str]],
|
||||
ssl_context: Optional[ssl.SSLContext] = None,
|
||||
) -> asyncio.Server:
|
||||
):
|
||||
logging.info(
|
||||
f"Starting SMTP server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
-i https://pypi.org/simple
|
||||
aiosmtpd==1.4.4.post2; python_version ~= '3.7'
|
||||
atpublic==4.0; python_version >= '3.8'
|
||||
attrs==23.2.0; python_version >= '3.7'
|
||||
python-jata==1.2; python_version >= '3.8'
|
||||
aiosmtpd==1.4.4.post2
|
||||
atpublic==4.0 ; python_version >= '3.8'
|
||||
attrs==23.1.0 ; python_version >= '3.7'
|
||||
python-jata==1.2
|
||||
|
@ -8,7 +8,7 @@ then
|
||||
tag_val=$(git describe --dirty=DIRTY --exact-match)
|
||||
case "$tag_val" in
|
||||
*DIRTY)
|
||||
echo "git-$commit-changes"
|
||||
echo "git=$commit-changes"
|
||||
;;
|
||||
v*) # Only consider tags starting with v
|
||||
echo "$tag_val"
|
||||
|
Reference in New Issue
Block a user