Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
2da8f13ebd | |||
8fe42e9163 | |||
937992a72e |
114
deploy_configs/README.md
Normal file
114
deploy_configs/README.md
Normal file
@ -0,0 +1,114 @@
|
||||
# Deployment command line example
|
||||
|
||||
Example terminal session for deploying. ssh to your VPS and follow along. Minor differences may be required. e.g. if you are already root, skip `sudo`. If curl is missing, use wget.
|
||||
|
||||
## Check python version
|
||||
|
||||
Python version should be a supported (as of now 3.9 and above)
|
||||
```sh
|
||||
python3 -V
|
||||
```
|
||||
|
||||
## Choose release
|
||||
```sh
|
||||
RELEASE=v1.0
|
||||
```
|
||||
|
||||
## Download App
|
||||
```sh
|
||||
curl -OL "https://gitea.balki.me/balki/mail4one/releases/download/$RELEASE/mail4one.pyz"
|
||||
chmod 555 mail4one.pyz
|
||||
```
|
||||
|
||||
## Download sample configurations
|
||||
```sh
|
||||
curl -OL "https://gitea.balki.me/balki/mail4one/raw/tag/$RELEASE/deploy_configs/mail4one.service"
|
||||
curl -OL "https://gitea.balki.me/balki/mail4one/raw/tag/$RELEASE/deploy_configs/mail4one.conf"
|
||||
curl -OL "https://gitea.balki.me/balki/mail4one/raw/tag/$RELEASE/deploy_configs/mail4one_cert_copy.sh"
|
||||
```
|
||||
|
||||
## Generate Password hash
|
||||
|
||||
This can be done in any machine. Do this once for each user. Every time a new hash is generated as a random salt is used. Even if you are using the same password for multiple clients, it is recommended to generate different hashes for each.
|
||||
|
||||
```sh
|
||||
./mail4one.pyz -g
|
||||
./mail4one.pyz -g <password> # also works but the password is saved in the shell commandline history
|
||||
```
|
||||
|
||||
## Generate config.json
|
||||
|
||||
Edit [config.sample](config.sample) in your local machine and convert to config.json (See [here](./config.sample#L5) for some tools).
|
||||
|
||||
Then copy the config.json to your vps
|
||||
```sh
|
||||
scp config.json user@vps:~/
|
||||
# or run below in vps terminal
|
||||
cat > config.json
|
||||
<paste json config from clibboard
|
||||
<Ctrl + D>
|
||||
|
||||
# move to /etc
|
||||
|
||||
# This should show number of lines in your config
|
||||
wc -l config.json
|
||||
sudo mv config.json /etc/mail4one/config.json
|
||||
```
|
||||
|
||||
## Create mail4one user
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /etc/sysusers.d/
|
||||
sudo cp mail4one.conf /etc/sysusers.d/
|
||||
sudo systemctl restart systemd-sysusers
|
||||
# This should show the new user created
|
||||
id mail4one
|
||||
```
|
||||
## Copy app
|
||||
```sh
|
||||
sudo cp mail4one.pyz /usr/local/bin/mail4one
|
||||
# This should show executable permissions and should be owned by root
|
||||
ls -l /usr/local/bin/mail4one
|
||||
```
|
||||
|
||||
## Setup mail4one.service
|
||||
```sh
|
||||
sudo cp mail4one.service /etc/systemd/system/mail4one.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now mail4one.service
|
||||
systemctl status mail4one
|
||||
```
|
||||
Above command should fail as the TLS certificates don't exist yet.
|
||||
|
||||
## 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`
|
||||
```sh
|
||||
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
|
||||
sudo cp mail4one_cert_copy.sh /etc/letsencrypt/renewal-hooks/deploy/
|
||||
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail4one_cert_copy.sh
|
||||
```
|
||||
## Restart service and check logs
|
||||
```sh
|
||||
sudo systemctl restart mail4one.service
|
||||
systemctl status mail4one.service
|
||||
cat /var/log/mail4one/mail4one.log
|
||||
```
|
||||
|
||||
## Testing dns and firewall
|
||||
In vps
|
||||
```sh
|
||||
mkdir test_dir
|
||||
touch test_dir/{a,b,c}
|
||||
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
|
||||
```sh
|
||||
curl http://mail.example.com:25
|
||||
```
|
||||
If not working, refer to VPS settings and OS firewall settings.
|
@ -1,17 +1,17 @@
|
||||
# NOTE: Sample config is provided in yaml format for easy editing
|
||||
# mail4one needs a json config, Please convert the config to json before passing to app
|
||||
# This is to avoid yaml depependency in the app
|
||||
# This is to avoid yaml dependency in the app
|
||||
#
|
||||
# Some tools to convert to json:
|
||||
# If you have go in your system (https://go.dev/)
|
||||
# If you have `go` in your system (https://go.dev/)
|
||||
# go run github.com/mikefarah/yq/v4@latest -oj -P . config.sample > config.json
|
||||
#
|
||||
# If you have pipx in your system (https://pypa.github.io/pipx/)
|
||||
# If you have `pipx` in your system (https://pypa.github.io/pipx/)
|
||||
# pipx run yq . config.sample > config.json
|
||||
#
|
||||
# or a browser:
|
||||
# https://onlineyamltools.com/convert-yaml-to-json
|
||||
#
|
||||
|
||||
default_tls: # Will be used by both pop and smtp servers
|
||||
# If using certbot(https://certbot.eff.org/),
|
||||
# the following files will be here /etc/letsencrypt/live/<domain name>
|
||||
|
@ -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=}"
|
||||
)
|
||||
|
Reference in New Issue
Block a user