7 Commits

7 changed files with 72 additions and 39 deletions

View File

@ -1,10 +1,10 @@
# Needs python3 >= 3.9, sed, git for build, docker for tests
build: clean
# Needs python3 >= 3.9, sed, git for build
mail4one.pyz: requirements.txt mail4one/*py
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/bin build/aiosmtpd/{docs,tests,qa}
rm -rf build/mail4one/__pycache__
rm -rf build/*.dist-info
python3 -m zipapp \
@ -13,10 +13,19 @@ build: clean
--main mail4one.server:main \
--compress build
.PHONY: build
build: clean mail4one.pyz
.PHONY: test
test: mail4one.pyz
PYTHONPATH=mail4one.pyz python3 -m unittest discover
.PHONY: clean
clean:
rm -rf build
rm -rf mail4one.pyz
.PHONY: 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.10-alpine sh scripts/runtests.sh
@ -31,24 +40,31 @@ docker-tests:
requirements.txt: Pipfile.lock
pipenv requirements > requirements.txt
.PHONY: format
format:
black mail4one/*py tests/*py
.PHONY: build-dev
build-dev: requirements.txt build
.PHONY: setup
setup:
pipenv install
.PHONY: cleanup
cleanup:
pipenv --rm
.PHONY: update
update:
rm requirements.txt Pipfile.lock
pipenv update
pipenv requirements > requirements.txt
.PHONY: shell
shell:
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
MYPYPATH=$(shell ls -d `pipenv --venv`/lib/python3*/site-packages) pipenv shell
test:
.PHONY: dev-test
dev-test:
pipenv run python -m unittest discover

View File

@ -1,4 +1,5 @@
import json
"""Module for parsing mail4one config.json"""
import re
import logging
from typing import Callable, Union, Optional
@ -56,13 +57,14 @@ class PopCfg(ServerCfg):
class SmtpStartTLSCfg(ServerCfg):
server_type = "smtp_starttls"
smtputf8 = True # Not used yet
require_starttls = True
smtputf8 = True
port = 25
class SmtpCfg(ServerCfg):
server_type = "smtp_starttls"
smtputf8 = True # Not used yet
server_type = "smtp"
smtputf8 = True
port = 465

View File

@ -2,18 +2,15 @@ import asyncio
import contextlib
import contextvars
import logging
import os
import ssl
import uuid
import random
from typing import Optional
from asyncio import StreamReader, StreamWriter
from dataclasses import dataclass
from hashlib import sha256
from pathlib import Path
from .config import User
from .pwhash import parse_hash, check_pass, PWInfo
from asyncio import StreamReader, StreamWriter
import random
from typing import Optional
from .poputils import (
InvalidCommand,
@ -316,12 +313,10 @@ async def start_session() -> None:
assert state().mbox
await transaction_stage()
logger.info(f"User:{state().username} done")
except ClientDisconnected as c:
except ClientDisconnected:
logger.info("Client disconnected")
pass
except ClientQuit:
logger.info("Client QUIT")
pass
except ClientError as c:
write(err("Something went wrong"))
logger.error(f"Unexpected client error: {c}")

View File

@ -1,11 +1,10 @@
import asyncio
import logging
import os
import ssl
import sys
from argparse import ArgumentParser
from pathlib import Path
from getpass import getpass
from typing import Optional, Union
from .smtp import create_smtp_server_starttls, create_smtp_server
from .pop3 import create_pop_server
@ -13,7 +12,6 @@ from .version import VERSION
from . import config
from . import pwhash
from typing import Optional, Union
def create_tls_context(certfile, keyfile) -> ssl.SSLContext:
@ -86,6 +84,8 @@ async def a_main(cfg: config.Config) -> None:
mails_path=Path(cfg.mails_path),
mbox_finder=mbox_finder,
ssl_context=stls_context,
require_starttls=stls.require_starttls,
smtputf8=stls.smtputf8,
)
servers.append(smtp_server_starttls)
elif scfg.server_type == "smtp":
@ -96,6 +96,7 @@ async def a_main(cfg: config.Config) -> None:
mails_path=Path(cfg.mails_path),
mbox_finder=mbox_finder,
ssl_context=get_tls_context(smtp.tls),
smtputf8=smtp.smtputf8,
)
servers.append(smtp_server)
else:

View File

@ -1,23 +1,18 @@
import asyncio
import io
import logging
import mailbox
import ssl
import uuid
import shutil
from functools import partial
from pathlib import Path
from typing import Callable, Optional
from . import config
from email.message import Message
import email.policy
from email.generator import BytesGenerator
import tempfile
import random
from aiosmtpd.handlers import Mailbox, AsyncMessage
from aiosmtpd.smtp import SMTP, DATA_SIZE_DEFAULT
from aiosmtpd.smtp import SMTP as SMTPServer
from aiosmtpd.handlers import AsyncMessage
from aiosmtpd.smtp import SMTP
from aiosmtpd.smtp import Envelope as SMTPEnvelope
from aiosmtpd.smtp import Session as SMTPSession
@ -31,7 +26,7 @@ class MyHandler(AsyncMessage):
self.mbox_finder = mbox_finder
async def handle_DATA(
self, server: SMTPServer, session: SMTPSession, envelope: SMTPEnvelope
self, server: SMTP, session: SMTPSession, envelope: SMTPEnvelope
) -> str:
self.rcpt_tos = envelope.rcpt_tos
self.peer = session.peer
@ -63,16 +58,20 @@ 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,
require_starttls: bool,
smtputf8: bool,
):
logger.info("Got smtp client cb starttls")
try:
handler = MyHandler(mails_path, mbox_finder)
smtp = SMTP(
handler=handler,
require_starttls=True,
require_starttls=require_starttls,
tls_context=context,
enable_SMTPUTF8=True,
enable_SMTPUTF8=smtputf8,
)
except:
logger.exception("Something went wrong")
@ -80,11 +79,13 @@ 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]], smtputf8: bool
):
logger.info("Got smtp client cb")
try:
handler = MyHandler(mails_path, mbox_finder)
smtp = SMTP(handler=handler, enable_SMTPUTF8=True)
smtp = SMTP(handler=handler, enable_SMTPUTF8=smtputf8)
except:
logger.exception("Something went wrong")
raise
@ -97,13 +98,22 @@ async def create_smtp_server_starttls(
mails_path: Path,
mbox_finder: Callable[[str], list[str]],
ssl_context: ssl.SSLContext,
require_starttls: bool,
smtputf8: bool,
) -> asyncio.Server:
logging.info(
f"Starting SMTP STARTTLS server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
)
loop = asyncio.get_event_loop()
return await loop.create_server(
partial(protocol_factory_starttls, mails_path, mbox_finder, ssl_context),
partial(
protocol_factory_starttls,
mails_path,
mbox_finder,
ssl_context,
require_starttls,
smtputf8,
),
host=host,
port=port,
start_serving=False,
@ -115,14 +125,15 @@ async def create_smtp_server(
port: int,
mails_path: Path,
mbox_finder: Callable[[str], list[str]],
ssl_context: Optional[ssl.SSLContext] = None,
ssl_context: Optional[ssl.SSLContext],
smtputf8: bool,
) -> asyncio.Server:
logging.info(
f"Starting SMTP server {host=}, {port=}, {mails_path=!s}, {ssl_context != None=}"
)
loop = asyncio.get_event_loop()
return await loop.create_server(
partial(protocol_factory, mails_path, mbox_finder),
partial(protocol_factory, mails_path, mbox_finder, smtputf8),
host=host,
port=port,
ssl=ssl_context,

View File

@ -89,6 +89,9 @@ class TestPop3(unittest.IsolatedAsyncioTestCase):
self.task = asyncio.create_task(pop_server.serve_forever())
self.reader, self.writer = await asyncio.open_connection("127.0.0.1", 7995)
# Additional writers to close
self.ws: list[asyncio.StreamWriter] = []
async def test_QUIT(self) -> None:
dialog = """
S: +OK Server Ready
@ -133,6 +136,7 @@ class TestPop3(unittest.IsolatedAsyncioTestCase):
async def test_dupe_AUTH(self) -> None:
r1, w1 = await asyncio.open_connection("127.0.0.1", 7995)
r2, w2 = await asyncio.open_connection("127.0.0.1", 7995)
self.ws += w1, w2
dialog = """
S: +OK Server Ready
C: USER foobar
@ -231,8 +235,10 @@ class TestPop3(unittest.IsolatedAsyncioTestCase):
async def asyncTearDown(self) -> None:
logging.debug("at teardown")
self.writer.close()
await self.writer.wait_closed()
for w in self.ws + [self.writer]:
w.close()
await w.wait_closed()
self.ws.clear()
self.task.cancel("test done")
async def dialog_checker(self, dialog: str) -> None:

View File

@ -33,6 +33,8 @@ class TestSMTP(unittest.IsolatedAsyncioTestCase):
port=7996,
mails_path=MAILS_PATH,
mbox_finder=lambda addr: [TEST_MBOX],
ssl_context=None,
smtputf8=True,
)
self.task = asyncio.create_task(smtp_server.serve_forever())