6 Commits

Author SHA1 Message Date
95423ebf63 Remove pyc files
pyc files are not compatible across multiple python versions
Harden systemd service files
2023-06-24 20:12:20 -04:00
2bcc807b91 Allow multiple servers of same type 2023-06-23 22:03:58 -04:00
bcd250d2b0 change STDOUT to CONSOLE 2023-06-23 21:42:00 -04:00
9107474d31 Add version info from git tags 2023-06-23 21:30:31 -04:00
2bf809c454 Remove PosixPath() in logs and add proper exception logging 2023-06-22 00:49:23 -04:00
1e6655a715 pop3 catch and log exceptions 2023-06-22 00:31:05 -04:00
9 changed files with 119 additions and 70 deletions

View File

@ -1,5 +1,3 @@
shell: shell:
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
@ -16,9 +14,10 @@ requirements.txt: Pipfile.lock
pipenv requirements > requirements.txt pipenv requirements > requirements.txt
build: clean requirements.txt build: clean requirements.txt
python3 -m pip install -r requirements.txt --target build python3 -m pip install -r requirements.txt --no-compile --target build
cp -r mail4one/ build/ cp -r mail4one/ build/
python3 -m compileall build/mail4one -f sed -i "s/DEVELOMENT/$(shell scripts/get_version.sh)/" build/mail4one/version.py
rm -rf build/mail4one/__pycache__
rm -rf build/*.dist-info rm -rf build/*.dist-info
python3 -m zipapp \ python3 -m zipapp \
--output mail4one.pyz \ --output mail4one.pyz \

View File

@ -9,14 +9,19 @@ Requires=network-online.target
[Service] [Service]
User=mail4one User=mail4one
ExecStart=/usr/local/bin/mail4one --config /etc/mail4one/config.json ExecStart=/usr/local/bin/mail4one --config /etc/mail4one/config.json
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE
StateDirectory=mail4one StateDirectory=mail4one/certs mail4one/mails
StateDirectoryMode=0750
UMask=
LogsDirectory=mail4one LogsDirectory=mail4one
WorkingDirectory=/var/lib/mail4one WorkingDirectory=/var/lib/mail4one
ProtectSystem=strict
PrivateTmp=true
PrivateUsers=true
ProtectHome=yes ProtectHome=yes
NoNewPrivileges=yes
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -10,7 +10,7 @@ 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
chmod 500 /var/lib/mail4one/certs chmod 750 /var/lib/mail4one/certs
chown mail4one:mail4one /var/lib/mail4one/certs chown mail4one:mail4one /var/lib/mail4one/certs
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/

View File

@ -41,6 +41,7 @@ class TLSCfg(Jata):
class ServerCfg(Jata): class ServerCfg(Jata):
server_type: str
host: str = "default" host: str = "default"
port: int port: int
# disabled: bool = False # disabled: bool = False
@ -48,22 +49,25 @@ class ServerCfg(Jata):
class PopCfg(ServerCfg): class PopCfg(ServerCfg):
server_type = "pop"
port = 995 port = 995
timeout_seconds = 60 timeout_seconds = 60
class SmtpStartTLSCfg(ServerCfg): class SmtpStartTLSCfg(ServerCfg):
smtputf8 = True server_type = "smtp_starttls"
smtputf8 = True # Not used yet
port = 25 port = 25
class SmtpCfg(ServerCfg): class SmtpCfg(ServerCfg):
smtputf8 = True server_type = "smtp_starttls"
smtputf8 = True # Not used yet
port = 465 port = 465
class LogCfg(Jata): class LogCfg(Jata):
logfile = "STDOUT" logfile = "CONSOLE"
level = "INFO" level = "INFO"
@ -77,10 +81,7 @@ class Config(Jata):
boxes: list[Mbox] boxes: list[Mbox]
users: list[User] users: list[User]
pop: PopCfg | None = None servers: list[ServerCfg]
smtp_starttls: SmtpStartTLSCfg | None = None
smtp: SmtpCfg | None = None
# smtp_port_submission = 587
CheckerFn = Callable[[str], bool] CheckerFn = Callable[[str], bool]

View File

@ -321,8 +321,8 @@ async def start_session() -> None:
except ClientError as c: except ClientError as c:
write(err("Something went wrong")) write(err("Something went wrong"))
logger.error(f"Unexpected client error: {c}") logger.error(f"Unexpected client error: {c}")
except Exception as e: except:
logger.error(f"Serious client error: {e}") logger.exception("Serious client error")
raise raise
finally: finally:
with contextlib.suppress(KeyError): with contextlib.suppress(KeyError):
@ -351,10 +351,13 @@ def make_pop_server_callback(mails_path: Path, users: list[User],
State(reader=reader, writer=writer, ip=ip, req_id=scfg.next_id())) State(reader=reader, writer=writer, ip=ip, req_id=scfg.next_id()))
logger.info(f"Got pop server callback") logger.info(f"Got pop server callback")
try: try:
return await asyncio.wait_for(start_session(), timeout_seconds) try:
finally: return await asyncio.wait_for(start_session(), timeout_seconds)
writer.close() finally:
await writer.wait_closed() writer.close()
await writer.wait_closed()
except:
logger.exception("unexpected exception")
return session_cb return session_cb
@ -368,7 +371,7 @@ async def create_pop_server(
timeout_seconds: int = 60, timeout_seconds: int = 60,
) -> asyncio.Server: ) -> asyncio.Server:
logging.info( logging.info(
f"Starting POP3 server {host=}, {port=}, {mails_path=}, {len(users)=}, {ssl_context != None=}, {timeout_seconds=}" f"Starting POP3 server {host=}, {port=}, {mails_path=!s}, {len(users)=}, {ssl_context != None=}, {timeout_seconds=}"
) )
return await asyncio.start_server( return await asyncio.start_server(
make_pop_server_callback(mails_path, users, timeout_seconds), make_pop_server_callback(mails_path, users, timeout_seconds),

View File

@ -9,6 +9,7 @@ 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 . import config from . import config
from . import pwhash from . import pwhash
@ -22,18 +23,20 @@ def create_tls_context(certfile, keyfile) -> ssl.SSLContext:
def setup_logging(cfg: config.LogCfg): def setup_logging(cfg: config.LogCfg):
logging_format = "%(asctime)s %(name)s %(levelname)s %(message)s @ %(filename)s:%(lineno)d" logging_format = "%(asctime)s %(name)s %(levelname)s %(message)s @ %(filename)s:%(lineno)d"
if cfg.logfile == "STDOUT": if cfg.logfile == "CONSOLE":
logging.basicConfig(level=cfg.level, format=logging_format) logging.basicConfig(level=cfg.level, format=logging_format)
else: else:
logging.basicConfig(filename=cfg.logfile, level=cfg.level, format=logging_format) logging.basicConfig(filename=cfg.logfile,
level=cfg.level,
format=logging_format)
async def a_main(cfg: config.Config) -> None: async def a_main(cfg: config.Config) -> None:
default_tls_context: ssl.SSLContext | None = None default_tls_context: ssl.SSLContext | None = None
if tls := cfg.default_tls: if tls := cfg.default_tls:
logging.info(f"Initializing default tls {tls.certfile=}, {tls.keyfile=}") logging.info(
f"Initializing default tls {tls.certfile=}, {tls.keyfile=}")
default_tls_context = create_tls_context(tls.certfile, tls.keyfile) default_tls_context = create_tls_context(tls.certfile, tls.keyfile)
def get_tls_context(tls: config.TLSCfg | str): def get_tls_context(tls: config.TLSCfg | str):
@ -42,7 +45,7 @@ async def a_main(cfg: config.Config) -> None:
elif tls == "disable": elif tls == "disable":
return None return None
else: else:
tls_cfg = config.TLSCfg(pop.tls) tls_cfg = config.TLSCfg(tls)
return create_tls_context(tls_cfg.certfile, tls_cfg.keyfile) return create_tls_context(tls_cfg.certfile, tls_cfg.keyfile)
def get_host(host): def get_host(host):
@ -54,47 +57,52 @@ async def a_main(cfg: config.Config) -> None:
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 cfg.pop: if not cfg.servers:
pop = config.PopCfg(cfg.pop) logging.warning("Nothing to do!")
pop_server = await create_pop_server( return
host=get_host(pop.host),
port=pop.port,
mails_path=Path(cfg.mails_path),
users=cfg.users,
ssl_context=get_tls_context(pop.tls),
timeout_seconds=pop.timeout_seconds,
)
servers.append(pop_server)
if cfg.smtp_starttls: for scfg in cfg.servers:
stls = config.SmtpStartTLSCfg(cfg.smtp_starttls) if scfg.server_type == "pop":
stls_context = get_tls_context(stls.tls) pop = config.PopCfg(scfg)
if not stls_context: pop_server = await create_pop_server(
raise Exception("starttls requires ssl_context") host=get_host(pop.host),
smtp_server_starttls = await create_smtp_server_starttls( port=pop.port,
host=get_host(stls.host), mails_path=Path(cfg.mails_path),
port=stls.port, users=cfg.users,
mails_path=Path(cfg.mails_path), ssl_context=get_tls_context(pop.tls),
mbox_finder=mbox_finder, timeout_seconds=pop.timeout_seconds,
ssl_context=stls_context, )
) servers.append(pop_server)
servers.append(smtp_server_starttls) elif scfg.server_type == "smtp_starttls":
stls = config.SmtpStartTLSCfg(scfg)
if cfg.smtp: stls_context = get_tls_context(stls.tls)
smtp = config.SmtpCfg(cfg.smtp) if not stls_context:
smtp_server = await create_smtp_server( raise Exception("starttls requires ssl_context")
host=get_host(smtp.host), smtp_server_starttls = await create_smtp_server_starttls(
port=smtp.port, host=get_host(stls.host),
mails_path=Path(cfg.mails_path), port=stls.port,
mbox_finder=mbox_finder, mails_path=Path(cfg.mails_path),
ssl_context=get_tls_context(smtp.tls), mbox_finder=mbox_finder,
) ssl_context=stls_context,
servers.append(smtp_server) )
servers.append(smtp_server_starttls)
elif scfg.server_type == "smtp":
smtp = config.SmtpCfg(scfg)
smtp_server = await create_smtp_server(
host=get_host(smtp.host),
port=smtp.port,
mails_path=Path(cfg.mails_path),
mbox_finder=mbox_finder,
ssl_context=get_tls_context(smtp.tls),
)
servers.append(smtp_server)
else:
logging.error(f"Unknown server {scfg.server_type=}")
if servers: if servers:
await asyncio.gather(*[server.serve_forever() for server in servers]) await asyncio.gather(*[server.serve_forever() for server in servers])
else: else:
logging.warn("Nothing to do!") logging.warning("Nothing to do!")
def main() -> None: def main() -> None:
@ -102,6 +110,7 @@ def main() -> None:
description="Personal Mail Server", description="Personal Mail Server",
epilog="See https://gitea.balki.me/balki/mail4one for more info", epilog="See https://gitea.balki.me/balki/mail4one for more info",
) )
parser.add_argument("-v", "--version", action="version", version=VERSION)
parser.add_argument( parser.add_argument(
"-e", "-e",
"--echo_password", "--echo_password",
@ -150,7 +159,7 @@ def main() -> None:
else: else:
cfg = config.Config(args.config.read_text()) cfg = config.Config(args.config.read_text())
setup_logging(config.LogCfg(cfg.logging)) setup_logging(config.LogCfg(cfg.logging))
logging.info(f"Starting mail4one {args.config=}") logging.info(f"Starting mail4one {VERSION} {args.config=!s}")
asyncio.run(a_main(cfg)) asyncio.run(a_main(cfg))

View File

@ -75,8 +75,8 @@ def protocol_factory_starttls(mails_path: Path,
tls_context=context, tls_context=context,
enable_SMTPUTF8=True, enable_SMTPUTF8=True,
) )
except Exception as e: except:
logger.error("Something went wrong", e) logger.exception("Something went wrong")
raise raise
return smtp return smtp
@ -87,8 +87,8 @@ def protocol_factory(mails_path: Path, mbox_finder: Callable[[str],
try: try:
handler = MyHandler(mails_path, mbox_finder) handler = MyHandler(mails_path, mbox_finder)
smtp = SMTP(handler=handler, enable_SMTPUTF8=True) smtp = SMTP(handler=handler, enable_SMTPUTF8=True)
except Exception as e: except:
logger.error("Something went wrong", e) logger.exception("Something went wrong")
raise raise
return smtp return smtp
@ -101,7 +101,7 @@ async def create_smtp_server_starttls(
ssl_context: ssl.SSLContext, ssl_context: ssl.SSLContext,
) -> asyncio.Server: ) -> asyncio.Server:
logging.info( logging.info(
f"Starting SMTP STARTTLS server {host=}, {port=}, {mails_path=}, {ssl_context != None=}" f"Starting SMTP STARTTLS server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
) )
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
return await loop.create_server( return await loop.create_server(
@ -121,7 +121,7 @@ async def create_smtp_server(
ssl_context: ssl.SSLContext | None = None, ssl_context: ssl.SSLContext | None = None,
) -> asyncio.Server: ) -> asyncio.Server:
logging.info( logging.info(
f"Starting SMTP server {host=}, {port=}, {mails_path=}, {ssl_context != None=}" f"Starting SMTP server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
) )
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
return await loop.create_server( return await loop.create_server(

2
mail4one/version.py Normal file
View File

@ -0,0 +1,2 @@
VERSION = "DEVELOMENT"

30
scripts/get_version.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/sh
commit=$(git rev-parse --short HEAD)
# This is true if there is a tag on current HEAD
if git describe --exact-match > /dev/null 2>&1
then
tag_val=$(git describe --dirty=DIRTY --exact-match)
case "$tag_val" in
*DIRTY)
echo "git=$commit-changes"
exit
;;
v*) # Only consider tags starting with v
echo "$tag_val"
;;
*)
echo "git-$commit"
esac
else
tag_val=$(git describe --dirty=DIRTY)
case "$tag_val" in
*DIRTY)
echo "git-$commit-changes"
;;
*)
echo "git-$commit"
esac
fi