3 Commits

7 changed files with 79 additions and 27 deletions

14
Pipfile.lock generated
View File

@ -18,20 +18,20 @@
"default": { "default": {
"aiosmtpd": { "aiosmtpd": {
"hashes": [ "hashes": [
"sha256:f821fe424b703b2ea391dc2df11d89d2afd728af27393e13cf1a3530f19fdc5e", "sha256:78d7b14f859ad0e6de252b47f9cf1ca6f1c82a8b0f10a9e39bec7e915a6aa5fe",
"sha256:f9243b7dfe00aaf567da8728d891752426b51392174a34d2cf5c18053b63dcbc" "sha256:a196922f1903e54c4d37c53415b7613056d39e2b1e8249f324b9ee7a439be0f1"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version ~= '3.7'", "markers": "python_version >= '3.8'",
"version": "==1.4.4.post2" "version": "==1.4.5"
}, },
"atpublic": { "atpublic": {
"hashes": [ "hashes": [
"sha256:0f40433219e124edf115c6c363808ca6f0e1cfa7d160d86b2fb94793086d1294", "sha256:d1c8cd931af7461f6d18bc6063383e8654d9e9ef19d58ee6dc01e8515bbf55df",
"sha256:80057c55641253b86dcb68b524f82328172371b6547d4c7462a9127fbfbbabfc" "sha256:df90de1162b1a941ee486f484691dc7c33123ee638ea5d6ca604061306e0fdde"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.0" "version": "==4.1.0"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [

View File

@ -81,15 +81,16 @@ systemctl status mail4one
Above command should fail as the TLS certificates don't exist yet. Above command should fail as the TLS certificates don't exist yet.
## Setup TLS certificates ## Setup TLS certificates
Install [certbot](https://certbot.eff.org/) and run below command. Follow instructions to create TLS certificates. Usually you want certificate for domain name like `mail.example.com` Install [certbot](https://certbot.eff.org/) and run below command. Follow instructions to create TLS certificates. Usually you want certificate for domain name like `mail.mydomain.com`
```sh ```sh
sudo certbot certonly sudo certbot certonly
sudo cp /etc/letsencrypt/live/mail.example.com/{fullchain,privkey}.pem /var/lib/mail4one/certs/
sudo chown mail4one:mail4one /var/lib/mail4one/certs/{fullchain,privkey}.pem
# Edit mail4one_cert_copy.sh to update your domain name # **Edit** mail4one_cert_copy.sh to update your domain name
sudo cp mail4one_cert_copy.sh /etc/letsencrypt/renewal-hooks/deploy/ sudo cp mail4one_cert_copy.sh /etc/letsencrypt/renewal-hooks/deploy/
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail4one_cert_copy.sh sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail4one_cert_copy.sh
# This will create and copy the certificates to the right path with correct permissions and ownership
sudo certbot certonly -d mail.mydomain.com --run-deploy-hooks --dry-run
``` ```
## Restart service and check logs ## Restart service and check logs
```sh ```sh
@ -109,6 +110,6 @@ 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.mydomain.com:25
``` ```
If not working, refer to VPS settings and OS firewall settings. If not working, refer to VPS settings and OS firewall settings.

View File

@ -7,13 +7,21 @@
# 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 -eu
if [ "$RENEWED_DOMAINS" = "mail.mydomain.com" ] if [ "$RENEWED_DOMAINS" = "mail.mydomain.com" ]
then then
mkdir -p /var/lib/mail4one/certs app=mail4one
chmod 750 /var/lib/mail4one/certs appuser=$app
chown mail4one:mail4one /var/lib/mail4one/certs certpath="/var/lib/$app/certs"
cp "$RENEWED_LINEAGE/fullchain.pem" /var/lib/mail4one/certs/
cp "$RENEWED_LINEAGE/privkey.pem" /var/lib/mail4one/certs/ mkdir -p "$certpath"
systemctl restart mail4one.service chmod 750 "$certpath"
echo "$(date) Renewed and deployed certificates for mail4one" >> /var/log/mail4one-cert-renew.log
chown $appuser:$appuser "$certpath"
install -o "$appuser" -g "$appuser" -m 444 "$RENEWED_LINEAGE/fullchain.pem" -t "$certpath"
install -o "$appuser" -g "$appuser" -m 400 "$RENEWED_LINEAGE/privkey.pem" -t "$certpath"
systemctl restart $app.service
echo "$(date) Renewed and deployed certificates for $app" >> /var/log/cert-renew.log
fi fi

View File

@ -29,7 +29,7 @@ from .poputils import (
end, end,
Request, Request,
MailEntry, MailEntry,
get_mail, get_mail_fp,
get_mails_list, get_mails_list,
MailList, MailList,
) )
@ -217,7 +217,12 @@ def trans_command_retr(mails: MailList, req: Request) -> None:
entry = mails.get(req.arg1) entry = mails.get(req.arg1)
if entry: if entry:
write(ok("Contents follow")) write(ok("Contents follow"))
write(get_mail(entry)) with get_mail_fp(entry) as fp:
for line in fp:
if line.startswith(b"."):
write(b".") # prepend dot
write(line)
# write(get_mail(entry)) # no prepend dot
write(end()) write(end())
mails.delete(req.arg1) mails.delete(req.arg1)
else: else:

View File

@ -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 contextlib import contextmanager
class ClientError(Exception): class ClientError(Exception):
@ -124,6 +125,12 @@ def set_nid(entries: list[MailEntry]):
entry.nid = i entry.nid = i
@contextmanager
def get_mail_fp(entry: MailEntry):
with open(entry.path, mode="rb") as fp:
yield fp
def get_mail(entry: MailEntry) -> bytes: def get_mail(entry: MailEntry) -> bytes:
with open(entry.path, mode="rb") as fp: with open(entry.path, mode="rb") as fp:
return fp.read() return fp.read()

View File

@ -1,5 +1,5 @@
-i https://pypi.org/simple -i https://pypi.org/simple
aiosmtpd==1.4.4.post2; python_version ~= '3.7' aiosmtpd==1.4.5; python_version >= '3.8'
atpublic==4.0; python_version >= '3.8' atpublic==4.1.0; python_version >= '3.8'
attrs==23.2.0; python_version >= '3.7' attrs==23.2.0; python_version >= '3.7'
python-jata==1.2; python_version >= '3.8' python-jata==1.2; python_version >= '3.8'

View File

@ -4,6 +4,7 @@ import logging
import tempfile import tempfile
import time import time
import os import os
import poplib
from mail4one.pop3 import create_pop_server from mail4one.pop3 import create_pop_server
from mail4one.config import User from mail4one.config import User
from pathlib import Path from pathlib import Path
@ -19,7 +20,13 @@ AQXWXSWNGAEPGIMG2F3QDKBXL3MRHY6K2BPID64ZR6LABLPVSF
TEST_USER = "foobar" TEST_USER = "foobar"
TEST_MBOX = "foobar_mails" TEST_MBOX = "foobar_mails"
USERS = [User(username=TEST_USER, password_hash=TEST_HASH, mbox=TEST_MBOX)] TEST_USER2 = "foo2"
TEST_MBOX2 = "foo2mails"
USERS = [
User(username=TEST_USER, password_hash=TEST_HASH, mbox=TEST_MBOX),
User(username=TEST_USER2, password_hash=TEST_HASH, mbox=TEST_MBOX2),
]
MAILS_PATH: Path MAILS_PATH: Path
@ -50,13 +57,21 @@ def setUpModule() -> None:
td = tempfile.TemporaryDirectory(prefix="m41.pop.") td = tempfile.TemporaryDirectory(prefix="m41.pop.")
unittest.addModuleCleanup(td.cleanup) unittest.addModuleCleanup(td.cleanup)
MAILS_PATH = Path(td.name) MAILS_PATH = Path(td.name)
os.mkdir(MAILS_PATH / TEST_MBOX) for mbox in (TEST_MBOX, TEST_MBOX2):
for md in ("new", "cur", "tmp"): os.mkdir(MAILS_PATH / mbox)
os.mkdir(MAILS_PATH / TEST_MBOX / md) for md in ("new", "cur", "tmp"):
os.mkdir(MAILS_PATH / mbox / md)
with open(MAILS_PATH / TEST_MBOX / "new/msg1.eml", "wb") as f: with open(MAILS_PATH / TEST_MBOX / "new/msg1.eml", "wb") as f:
f.write(TESTMAIL) f.write(TESTMAIL)
with open(MAILS_PATH / TEST_MBOX / "new/msg2.eml", "wb") as f: with open(MAILS_PATH / TEST_MBOX / "new/msg2.eml", "wb") as f:
f.write(TESTMAIL) f.write(TESTMAIL)
with open(MAILS_PATH / TEST_MBOX2 / "new/msg1.eml", "wb") as f:
f.write(TESTMAIL)
f.write(b"More lines to follow\r\n")
f.write(b".Line starts with a dot\r\n")
f.write(b"some more lines\r\n")
f.write(b".\r\n")
f.write(b"Previous line just has a dot\r\n")
logging.debug(MAILS_PATH) logging.debug(MAILS_PATH)
@ -198,6 +213,22 @@ class TestPop3(unittest.IsolatedAsyncioTestCase):
""" """
await self.dialog_checker(dialog) await self.dialog_checker(dialog)
async def test_poplib(self) -> None:
def run_poplib():
pc = poplib.POP3("127.0.0.1", 7995)
try:
self.assertEqual(b"+OK Server Ready", pc.getwelcome())
self.assertEqual(b"+OK Welcome", pc.user("foo2"))
self.assertEqual(b"+OK Login successful", pc.pass_("helloworld"))
_, eml, oc = pc.retr(1)
self.assertIn(b"Previous line just has a dot", eml)
self.assertIn(b".Line starts with a dot", eml)
self.assertIn(b".", eml)
finally:
pc.quit()
await asyncio.to_thread(run_poplib)
async def asyncTearDown(self) -> None: async def asyncTearDown(self) -> None:
logging.debug("at teardown") logging.debug("at teardown")
self.writer.close() self.writer.close()