Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
b9fbc9138c | |||
5469659f1e | |||
5f56a1256b | |||
59abf24ce5 | |||
7cb1b69744 | |||
95423ebf63 | |||
2bcc807b91 | |||
bcd250d2b0 | |||
9107474d31 | |||
2bf809c454 | |||
1e6655a715 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,4 +3,5 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
dummy.py
|
dummy.py
|
||||||
build
|
build
|
||||||
mail4one.pyz
|
mail4one*.pyz
|
||||||
|
deploy_configs/config.json
|
||||||
|
54
Makefile
54
Makefile
@ -1,24 +1,9 @@
|
|||||||
|
# Needs python3 >= 3.9, sed, git for build
|
||||||
|
build: clean
|
||||||
shell:
|
python3 -m pip install -r requirements.txt --no-compile --target build
|
||||||
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
|
|
||||||
|
|
||||||
test:
|
|
||||||
pipenv run python -m unittest discover
|
|
||||||
|
|
||||||
docker-tests:
|
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm -it python:3.11-alpine sh runtests.sh
|
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm -it python:3.10-alpine sh runtests.sh
|
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm -it python:3.11 sh runtests.sh
|
|
||||||
docker run --pull=always -v `pwd`:/app -w /app --rm -it python:3.10 sh runtests.sh
|
|
||||||
|
|
||||||
requirements.txt: Pipfile.lock
|
|
||||||
pipenv requirements > requirements.txt
|
|
||||||
|
|
||||||
build: clean requirements.txt
|
|
||||||
python3 -m pip install -r requirements.txt --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 \
|
||||||
@ -29,3 +14,32 @@ build: clean requirements.txt
|
|||||||
clean:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
rm -rf mail4one.pyz
|
rm -rf mail4one.pyz
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
requirements.txt: Pipfile.lock
|
||||||
|
pipenv requirements > requirements.txt
|
||||||
|
|
||||||
|
format:
|
||||||
|
black mail4one/*py
|
||||||
|
|
||||||
|
build-dev: requirements.txt build
|
||||||
|
|
||||||
|
setup:
|
||||||
|
pipenv install
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
pipenv --rm
|
||||||
|
|
||||||
|
shell:
|
||||||
|
MYPYPATH=`pipenv --venv`/lib/python3.11/site-packages pipenv shell
|
||||||
|
|
||||||
|
test:
|
||||||
|
pipenv run python -m unittest discover
|
||||||
|
88
README.md
88
README.md
@ -1,44 +1,70 @@
|
|||||||
# mail4one
|
# Mail4one
|
||||||
|
|
||||||
Mail server for single user #asyncio #python
|
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.
|
||||||
|
|
||||||
## Features
|
# Getting started
|
||||||
|
|
||||||
* smtp server with STARTTLS
|
1. Get a domain name
|
||||||
* pop3 server with TLS
|
1. Get a VPS (or a home server). Setup firewall rules for receive on port 25, 995, 465
|
||||||
* Both running on single thread using asyncio
|
1. Setup [MX record](#dns-records-receiving)
|
||||||
* Saves mails in simple Maildir format (i.e one file per email message)
|
1. [Build](#building-from-source) / Download latest release - `mail4one.pyz`
|
||||||
* After opening port, drops root privileges. So the process will not running as `nobody`
|
1. Generate `config.json` from [config.sample](deploy_configs/config.sample)
|
||||||
|
1. Run `./mail4one.pyz -c config.json`
|
||||||
|
1. [Optional] Setup systemd service and TLS certificates. See [deploy_configs](deploy_configs/) for examples
|
||||||
|
|
||||||
## How to use
|
# Sending email
|
||||||
|
|
||||||
echo -n "balki is awesome+<YOUR PASSWORD>" | sha256sum
|
Mail4one only takes care of receiving and serving email. For sending email, use an external service like below
|
||||||
pipenv install
|
|
||||||
sudo $(pipenv --venv)/bin/python ./run.py --certfile /etc/letsencrypt/live/your.domain.com/fullchain.pem --keyfile /etc/letsencrypt/live/your.domain.com/privkey.pem /var/mails --password_hash <PASSWORD_HASH_FROM_ABOVE>
|
|
||||||
|
|
||||||
## Just pop server for debugging
|
* https://www.smtp2go.com/pricing/
|
||||||
|
* https://www.mailgun.com/pricing/
|
||||||
|
* https://sendgrid.com/free/
|
||||||
|
|
||||||
pipenv run python -m mail4one.pop3 /path/to/mails 9995 your_password
|
Most of them have generous free tier which is more than enough for personal use.
|
||||||
|
|
||||||
## Nextups
|
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.
|
||||||
|
|
||||||
* Support sending emails - Also support for popular services like mailgun/sendgrid
|
# Community
|
||||||
* Smart assistant like functionality. For e.g.
|
|
||||||
* You don't need all emails of package deliver status. Just the latest one would be enough.
|
|
||||||
* Some type of emails can auto expire. Old newsletters are not very helpful
|
|
||||||
* Aggregate emails for weekend reading.
|
|
||||||
* Small webserver
|
|
||||||
* SPAM filtering - not that important as you can use unique addresses for each service. e.g. facebook@mydomian.com, bankac@mydomain.com, reddit@mydomain.com etc. You can easily figure out who sold your address to spammers and block it.
|
|
||||||
|
|
||||||
## Goals
|
Original source is at https://gitea.balki.me/balki/mail4one
|
||||||
* Intended to be used for one person. So won't have features that don't make sense in this context. e.g. LDAP AUTH, Mail quota, etc,
|
|
||||||
* Supports only python3.7. No plans to support older versions
|
|
||||||
|
|
||||||
## Known to work
|
For issues, pull requests, discussions, please use github mirror: https://github.com/mail4one/mail4one
|
||||||
* Server: Google Cloud f1-micro with Ubuntu 18.04 - Always Free instance
|
|
||||||
* Clients: thunderbird, evolution, k9mail
|
|
||||||
* smtp: Received email from all. Didn't see any drops. Tested from gmail, protonmail, reddit and few others
|
|
||||||
|
|
||||||
## Contribution
|
# Documentation
|
||||||
|
|
||||||
Pull requests and issues welcome
|
See files under [deploy_configs](deploy_configs/) for configuring and deploying to a standard systemd based linux system (e.g. debian, ubuntu, fedora, archlinux etc). [config.sample](deploy_configs/config.sample) has inline comments for more details. Feel free create github issue/discussions for support.
|
||||||
|
|
||||||
|
## DNS Records (Receiving)
|
||||||
|
|
||||||
|
If you want to receive email for `john@example.com` and your VPS IP address is `1.2.3.4`. Following record needs to be created
|
||||||
|
|
||||||
|
|Type | Name | Target | Notes |
|
||||||
|
|------|------------------|----------------------|------------------------------------------------------|
|
||||||
|
| A | mail.example.com | `1.2.3.4` | |
|
||||||
|
| AAAA | mail.example.com | `abcd:1234::1234::1` | Optional, add if available |
|
||||||
|
| MX | example.com | `mail.example.com` | |
|
||||||
|
| MX | sub.example.com | `mail.example.com` | Optional, to receive emails like foo@sub.example.com |
|
||||||
|
|
||||||
|
For sending emails `DMARC`, `DKIM` and `SPF` records need to be set. Please refer to email [sending](#sending-email) provider for details.
|
||||||
|
|
||||||
|
# Building from source
|
||||||
|
|
||||||
|
Make sure to have make, git, python >= 3.9, and pip installed in your system and run below
|
||||||
|
|
||||||
|
make build
|
||||||
|
|
||||||
|
This should generate `mail4one.pyz` in current folder. This is a [executable python archive](https://docs.python.org/3/library/zipapp.html). Should be runnable as `./mail4one.pyz` or as `python3 mail4one.pyz`.
|
||||||
|
|
||||||
|
# Roadmap (Planned features for future)
|
||||||
|
|
||||||
|
* Other ways to install and update (PIP, AUR, docker etc)
|
||||||
|
* 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
|
||||||
|
* Support email submission from client to forward to other senders or direct delivery
|
||||||
|
* Optional SPAM filtering
|
||||||
|
* Optional DMARC,SPF,DKIM verification
|
||||||
|
* Webmail Client
|
||||||
|
* Web UI to view graphs and smart reports
|
||||||
|
33
TODO.md
33
TODO.md
@ -1,33 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
DEV
|
|
||||||
1. unittests
|
|
||||||
1. Web interface
|
|
||||||
1. Custom email processing
|
|
||||||
1. Refactor smtp controller
|
|
||||||
1. pip installable package
|
|
||||||
1. Improve logging with timestamp, peer info, stats
|
|
||||||
1. custom smtp with auth
|
|
||||||
1. Filter domain name
|
|
||||||
2. Support multiple subdomains and save in different folders
|
|
||||||
1. smtp2go integration
|
|
||||||
1. metrics
|
|
||||||
2. imap
|
|
||||||
3. handle connection disconnect
|
|
||||||
4. rethink deleted items
|
|
||||||
|
|
||||||
MASTER
|
|
||||||
1. tests!
|
|
||||||
2. docker support
|
|
||||||
3. multi domain
|
|
||||||
4. [pop] auto delete
|
|
||||||
5. [pop] TOP command
|
|
||||||
6. merge with cloud_test branch
|
|
||||||
|
|
||||||
7. User timeout for POP
|
|
||||||
8. unittests
|
|
||||||
9. Web interface
|
|
||||||
10. Custom email processing
|
|
||||||
11. Refactor smtp controller
|
|
||||||
12. pip installable package
|
|
||||||
13. Listen on port 465 for smtp too
|
|
144
deploy_configs/config.sample
Normal file
144
deploy_configs/config.sample
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# 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
|
||||||
|
#
|
||||||
|
# Some tools to convert to json:
|
||||||
|
# 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/)
|
||||||
|
# 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>
|
||||||
|
# Use mail4one_cert_copy.sh to automaticallly copy on renewal
|
||||||
|
certfile: /var/lib/mail4one/certs/fullchain.pem
|
||||||
|
keyfile: /var/lib/mail4one/certs/privkey.pem
|
||||||
|
|
||||||
|
# default_host: '0.0.0.0'
|
||||||
|
|
||||||
|
logging:
|
||||||
|
# Setup logrotate(https://github.com/logrotate/logrotate) if needed
|
||||||
|
logfile: /var/log/mail4one/mail4one.log
|
||||||
|
|
||||||
|
mails_path: /var/lib/mail4one/mails
|
||||||
|
|
||||||
|
matches:
|
||||||
|
# only <to> address is matched. (sent by smtp RCPT command)
|
||||||
|
# address is converted to lowercase before matching
|
||||||
|
- name: example.com
|
||||||
|
addr_rexs:
|
||||||
|
- .*@example.com
|
||||||
|
|
||||||
|
- name: promotion-spammers
|
||||||
|
addrs:
|
||||||
|
- twitter@example.com
|
||||||
|
- random-app-not-used-anymore@example.com
|
||||||
|
- flyer-walmart@example.com
|
||||||
|
|
||||||
|
- name: john
|
||||||
|
addrs:
|
||||||
|
- john.doe@example.com # Tip: Dont use this. Always use a different alias, this way there is no address for spammers
|
||||||
|
- secret.name@example.com
|
||||||
|
- john.facebook@example.com
|
||||||
|
|
||||||
|
- name: jane
|
||||||
|
addrs:
|
||||||
|
- jane.doe@example.com
|
||||||
|
- jane.instagram@example.com
|
||||||
|
|
||||||
|
- name: jane-all
|
||||||
|
addr_rexs:
|
||||||
|
- jane.*@example.com
|
||||||
|
|
||||||
|
- name: shared
|
||||||
|
addrs:
|
||||||
|
- kidschool@example.com
|
||||||
|
- mortgage@example.com
|
||||||
|
- water@example.com
|
||||||
|
- electricity@example.com
|
||||||
|
- airbnb@example.com
|
||||||
|
|
||||||
|
boxes:
|
||||||
|
# Mails saved in maildir format under <mails_path>/<name>/new
|
||||||
|
- name: default_null_mbox # Means, the mails are dropped
|
||||||
|
rules:
|
||||||
|
- match_name: example.com
|
||||||
|
negate: true # Anything mail that does not match '.*@example.com'
|
||||||
|
stop_check: true # No further rules will be checked, mail is dropped
|
||||||
|
|
||||||
|
# Mailbox to store non-interesting emails but occasionally have a useful mail
|
||||||
|
# Create a second account in your email client and disable notification
|
||||||
|
- name: promotion-box
|
||||||
|
rules:
|
||||||
|
- match_name: promotion-spammers
|
||||||
|
stop_check: true
|
||||||
|
|
||||||
|
- name: johnsmbox
|
||||||
|
rules:
|
||||||
|
- match_name: john
|
||||||
|
- match_name: shared
|
||||||
|
## To receive all emails excluding jane's personal emails
|
||||||
|
# - match_name: jane
|
||||||
|
# negate: true
|
||||||
|
|
||||||
|
- name: janesmbox
|
||||||
|
rules:
|
||||||
|
- match_name: jane
|
||||||
|
- match_name: shared
|
||||||
|
- match_name: jane-all
|
||||||
|
|
||||||
|
- name: all
|
||||||
|
rules:
|
||||||
|
# matches all emails except those are not for 'example.com', which are dropped before
|
||||||
|
- match_name: default_match_all
|
||||||
|
|
||||||
|
users: # Used only by the pop server, smtp is for receiving mails only. No auth is used
|
||||||
|
- username: johnmobile
|
||||||
|
# Generated using below command. Will produce different hash each time (salt is random)
|
||||||
|
# ./mail4one.pyz -g johnsecretpassword
|
||||||
|
password_hash: AEH6JG3IZR3ASA2ORJHQ62YTR6PHFRP6PAXQ6RI2VZFXAT5M6VAATE373PGCUHBJTLIDOQV6UJKICP2JTKDE3QXP7ROJ227QYFQDAXPP4LY4TLPTEHUZG7D7X6VKWZ4BVCASYCD3SSNQ555AZPIFMDAV
|
||||||
|
mbox: johnsmbox
|
||||||
|
|
||||||
|
# **NOTE** Use different username for each email client.
|
||||||
|
# Otherwise emails may appear in only one client
|
||||||
|
- username: johnlaptop
|
||||||
|
password_hash: AEH6JG3IZR3ASA2ORJHQ62YTR6PHFRP6PAXQ6RI2VZFXAT5M6VAATE373PGCUHBJTLIDOQV6UJKICP2JTKDE3QXP7ROJ227QYFQDAXPP4LY4TLPTEHUZG7D7X6VKWZ4BVCASYCD3SSNQ555AZPIFMDAV
|
||||||
|
mbox: johnsmbox
|
||||||
|
|
||||||
|
# Second account to not clutter main mailbox. Disable notification for this account
|
||||||
|
- username: john-mobile-promotion
|
||||||
|
password_hash: AGBD47ZYBA7BMUQY25YDTYQWVPJFDBTLIICKFP2IL2GI4M7AO2LIVIZXTY6N25KBRLOEC7TLXGAFW7SSQEBKCG7U3FJNKW6RZWZBS3ABSP2U53BBIOCXZNWPXJGWAQ6WFXIF7T4YQJZD5QLF2OO4JZ45
|
||||||
|
mbox: promotion-box
|
||||||
|
|
||||||
|
- username: janemobile
|
||||||
|
password_hash: AGQNPATXU7PP7LDD6DZ4HFLUUHRJDHFQKKKRLVLGOIIEHC7TPOZF7NTXGDAIGDNHF62RAH4N44DB46O3VC4TBOLE5XHY6S77YPLTWCNAHGONEOZYO6YWJ3NHLKOHFJLF6BOHNMCI3RCPWXWXQPHSFDQR
|
||||||
|
mbox: janesmbox
|
||||||
|
|
||||||
|
- username: family_desktop # Catch all for backup
|
||||||
|
password_hash: AGBD47ZYBA7BMUQY25YDTYQWVPJFDBTLIICKFP2IL2GI4M7AO2LIVIZXTY6N25KBRLOEC7TLXGAFW7SSQEBKCG7U3FJNKW6RZWZBS3ABSP2U53BBIOCXZNWPXJGWAQ6WFXIF7T4YQJZD5QLF2OO4JZ45
|
||||||
|
mbox: all
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- server_type: pop
|
||||||
|
## default values
|
||||||
|
# port: 995
|
||||||
|
# host: '0.0.0.0'
|
||||||
|
# tls: default # Uses default_tls config
|
||||||
|
- server_type: smtp
|
||||||
|
## default values
|
||||||
|
# port: 465
|
||||||
|
# host: '0.0.0.0'
|
||||||
|
# tls: default # Uses default_tls config
|
||||||
|
# tls: disable # disable tls and receive emails in plain text only
|
||||||
|
- server_type: smtp_starttls
|
||||||
|
## default values
|
||||||
|
# port: 25
|
||||||
|
# host: '0.0.0.0'
|
||||||
|
# tls: default # Uses default_tls config
|
||||||
|
|
||||||
|
# vim: ft=yaml
|
@ -1,3 +1,7 @@
|
|||||||
|
# This file should be copied to /etc/sysusers.d/mail4one.conf
|
||||||
|
# Then either restart the system or run `systemctl restart systemd-sysusers`
|
||||||
|
# That should create a system user 'mail4one'
|
||||||
|
#
|
||||||
# See sysusers.d(5) for details.
|
# See sysusers.d(5) for details.
|
||||||
|
|
||||||
u mail4one - "Personal Mail server"
|
u mail4one - "Personal Mail server"
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
# mail4one.service
|
# This file should be copied to /etc/systemd/system/mail4one.service
|
||||||
|
# Quickstart
|
||||||
|
# systemctl daemon-reload
|
||||||
|
# systemctl enable --now mail4one.service
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Personal Mail server
|
Description=Personal Mail server
|
||||||
@ -7,16 +10,25 @@ After=network.target network-online.target
|
|||||||
Requires=network-online.target
|
Requires=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|
||||||
|
# This user should already exist. See mail4one.conf for creating user with sysusers
|
||||||
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
|
|
||||||
|
|
||||||
StateDirectory=mail4one
|
# Below allows to bind to port < 1024. Standard ports are 25, 465, 995
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
StateDirectory=mail4one/certs mail4one/mails
|
||||||
|
StateDirectoryMode=0750
|
||||||
LogsDirectory=mail4one
|
LogsDirectory=mail4one
|
||||||
WorkingDirectory=/var/lib/mail4one
|
WorkingDirectory=/var/lib/mail4one
|
||||||
|
|
||||||
|
ProtectSystem=strict
|
||||||
|
PrivateTmp=true
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
|
ProtectProc=invisible
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# certbot deploy hook to copy certificates to mail4one when renewed.
|
# certbot deploy hook to copy certificates to mail4one when renewed.
|
||||||
|
# Initial setup, Install certbot(https://certbot.eff.org/) and run `certbot certonly` as root
|
||||||
|
#
|
||||||
# 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
|
||||||
|
|
||||||
@ -10,7 +12,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/
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from .server import main
|
|
||||||
|
|
||||||
main()
|
|
@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
from typing import Callable, Union, Optional
|
||||||
from jata import Jata, MutableDefault
|
from jata import Jata, MutableDefault
|
||||||
|
|
||||||
|
|
||||||
@ -41,46 +41,47 @@ 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
|
||||||
tls: TLSCfg | str = "default"
|
tls: Union[TLSCfg, str] = "default"
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
|
||||||
class Config(Jata):
|
class Config(Jata):
|
||||||
default_tls: TLSCfg | None
|
default_tls: Optional[TLSCfg] = None
|
||||||
default_host: str = "0.0.0.0"
|
default_host: str = "0.0.0.0"
|
||||||
logging: LogCfg | None = None
|
logging: Optional[LogCfg] = None
|
||||||
|
|
||||||
mails_path: str
|
mails_path: str
|
||||||
matches: list[Match]
|
matches: list[Match]
|
||||||
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]
|
||||||
@ -88,7 +89,6 @@ 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):
|
def make_match_fn(m: Match):
|
||||||
if m.addrs and m.addr_rexs:
|
if m.addrs and m.addr_rexs:
|
||||||
raise Exception("Both addrs and addr_rexs is set")
|
raise Exception("Both addrs and addr_rexs is set")
|
||||||
@ -96,8 +96,7 @@ def parse_checkers(cfg: Config) -> list[Checker]:
|
|||||||
return lambda malias: malias in m.addrs
|
return lambda malias: malias in m.addrs
|
||||||
elif m.addr_rexs:
|
elif m.addr_rexs:
|
||||||
compiled_res = [re.compile(reg) for reg in m.addr_rexs]
|
compiled_res = [re.compile(reg) for reg in m.addr_rexs]
|
||||||
return lambda malias: any(
|
return lambda malias: any(reg.match(malias) for reg in compiled_res)
|
||||||
reg.match(malias) for reg in compiled_res)
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Neither addrs nor addr_rexs is set")
|
raise Exception("Neither addrs nor addr_rexs is set")
|
||||||
|
|
||||||
@ -113,13 +112,13 @@ def parse_checkers(cfg: Config) -> list[Checker]:
|
|||||||
return mbox_name, match_fn, rule.stop_check
|
return mbox_name, match_fn, rule.stop_check
|
||||||
|
|
||||||
return [
|
return [
|
||||||
make_checker(mbox.name, Rule(rule)) for mbox in cfg.boxes or []
|
make_checker(mbox.name, Rule(rule))
|
||||||
|
for mbox in cfg.boxes or []
|
||||||
for rule in mbox.rules
|
for rule in mbox.rules
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_mboxes(addr: str, checks: list[Checker]) -> list[str]:
|
def get_mboxes(addr: str, checks: list[Checker]) -> list[str]:
|
||||||
|
|
||||||
def inner():
|
def inner():
|
||||||
for mbox, match_fn, stop_check in checks:
|
for mbox, match_fn, stop_check in checks:
|
||||||
if match_fn(addr):
|
if match_fn(addr):
|
||||||
|
@ -13,6 +13,8 @@ from .pwhash import parse_hash, check_pass, PWInfo
|
|||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .poputils import (
|
from .poputils import (
|
||||||
InvalidCommand,
|
InvalidCommand,
|
||||||
parse_command,
|
parse_command,
|
||||||
@ -44,7 +46,6 @@ class State:
|
|||||||
|
|
||||||
|
|
||||||
class SharedState:
|
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.mails_path = mails_path
|
||||||
self.users = users
|
self.users = users
|
||||||
@ -56,8 +57,7 @@ class SharedState:
|
|||||||
return self.counter
|
return self.counter
|
||||||
|
|
||||||
|
|
||||||
c_shared_state: contextvars.ContextVar = contextvars.ContextVar(
|
c_shared_state: contextvars.ContextVar = contextvars.ContextVar("pop_shared_state")
|
||||||
"pop_shared_state")
|
|
||||||
|
|
||||||
|
|
||||||
def scfg() -> SharedState:
|
def scfg() -> SharedState:
|
||||||
@ -72,7 +72,6 @@ def state() -> State:
|
|||||||
|
|
||||||
|
|
||||||
class PopLogger(logging.LoggerAdapter):
|
class PopLogger(logging.LoggerAdapter):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(logging.getLogger("pop3"), None)
|
super().__init__(logging.getLogger("pop3"), None)
|
||||||
|
|
||||||
@ -280,8 +279,7 @@ def get_deleted_items(deleted_items_path: Path) -> set[str]:
|
|||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def save_deleted_items(deleted_items_path: Path,
|
def save_deleted_items(deleted_items_path: Path, deleted_items: set[str]) -> None:
|
||||||
deleted_items: set[str]) -> None:
|
|
||||||
with deleted_items_path.open(mode="w") as f:
|
with deleted_items_path.open(mode="w") as f:
|
||||||
f.writelines(f"{did}\n" for did in deleted_items)
|
f.writelines(f"{did}\n" for did in deleted_items)
|
||||||
|
|
||||||
@ -298,8 +296,9 @@ async def transaction_stage() -> None:
|
|||||||
new_deleted_items: set[str] = await process_transactions(mails_list)
|
new_deleted_items: set[str] = await process_transactions(mails_list)
|
||||||
logger.info(f"completed transactions. Deleted:{len(new_deleted_items)}")
|
logger.info(f"completed transactions. Deleted:{len(new_deleted_items)}")
|
||||||
if new_deleted_items:
|
if new_deleted_items:
|
||||||
save_deleted_items(deleted_items_path,
|
save_deleted_items(
|
||||||
existing_deleted_items.union(new_deleted_items))
|
deleted_items_path, existing_deleted_items.union(new_deleted_items)
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Saved deleted items")
|
logger.info(f"Saved deleted items")
|
||||||
|
|
||||||
@ -321,8 +320,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):
|
||||||
@ -330,7 +329,6 @@ async def start_session() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def parse_users(users: list[User]) -> dict[str, tuple[PWInfo, str]]:
|
def parse_users(users: list[User]) -> dict[str, tuple[PWInfo, str]]:
|
||||||
|
|
||||||
def inner():
|
def inner():
|
||||||
for user in users:
|
for user in users:
|
||||||
user = User(user)
|
user = User(user)
|
||||||
@ -340,21 +338,22 @@ def parse_users(users: list[User]) -> dict[str, tuple[PWInfo, str]]:
|
|||||||
return dict(inner())
|
return dict(inner())
|
||||||
|
|
||||||
|
|
||||||
def make_pop_server_callback(mails_path: Path, users: list[User],
|
def make_pop_server_callback(mails_path: Path, users: list[User], timeout_seconds: int):
|
||||||
timeout_seconds: int):
|
|
||||||
scfg = SharedState(mails_path=mails_path, users=parse_users(users))
|
scfg = SharedState(mails_path=mails_path, users=parse_users(users))
|
||||||
|
|
||||||
async def session_cb(reader: StreamReader, writer: StreamWriter):
|
async def session_cb(reader: StreamReader, writer: StreamWriter):
|
||||||
c_shared_state.set(scfg)
|
c_shared_state.set(scfg)
|
||||||
ip, _ = writer.get_extra_info("peername")
|
ip, _ = writer.get_extra_info("peername")
|
||||||
c_state.set(
|
c_state.set(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:
|
try:
|
||||||
return await asyncio.wait_for(start_session(), timeout_seconds)
|
return await asyncio.wait_for(start_session(), timeout_seconds)
|
||||||
finally:
|
finally:
|
||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
except:
|
||||||
|
logger.exception("unexpected exception")
|
||||||
|
|
||||||
return session_cb
|
return session_cb
|
||||||
|
|
||||||
@ -364,11 +363,11 @@ async def create_pop_server(
|
|||||||
port: int,
|
port: int,
|
||||||
mails_path: Path,
|
mails_path: Path,
|
||||||
users: list[User],
|
users: list[User],
|
||||||
ssl_context: ssl.SSLContext | None = None,
|
ssl_context: Optional[ssl.SSLContext] = None,
|
||||||
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),
|
||||||
|
@ -130,7 +130,6 @@ def get_mail(entry: MailEntry) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
class MailList:
|
class MailList:
|
||||||
|
|
||||||
def __init__(self, entries: list[MailEntry]):
|
def __init__(self, entries: list[MailEntry]):
|
||||||
self.entries = entries
|
self.entries = entries
|
||||||
set_nid(self.entries)
|
set_nid(self.entries)
|
||||||
|
@ -19,17 +19,13 @@ KEY_LEN = 64 # This is python default
|
|||||||
|
|
||||||
def gen_pwhash(password: str) -> str:
|
def gen_pwhash(password: str) -> str:
|
||||||
salt = os.urandom(SALT_LEN)
|
salt = os.urandom(SALT_LEN)
|
||||||
sh = scrypt(password.encode(),
|
sh = scrypt(
|
||||||
salt=salt,
|
password.encode(), salt=salt, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P, dklen=KEY_LEN
|
||||||
n=SCRYPT_N,
|
)
|
||||||
r=SCRYPT_R,
|
|
||||||
p=SCRYPT_P,
|
|
||||||
dklen=KEY_LEN)
|
|
||||||
return b32encode(VERSION + salt + sh).decode()
|
return b32encode(VERSION + salt + sh).decode()
|
||||||
|
|
||||||
|
|
||||||
class PWInfo:
|
class PWInfo:
|
||||||
|
|
||||||
def __init__(self, salt: bytes, sh: bytes):
|
def __init__(self, salt: bytes, sh: bytes):
|
||||||
self.salt = salt
|
self.salt = salt
|
||||||
self.scrypt_hash = sh
|
self.scrypt_hash = sh
|
||||||
@ -40,7 +36,8 @@ def parse_hash(pwhash_str: str) -> PWInfo:
|
|||||||
|
|
||||||
if not len(pwhash) == 1 + SALT_LEN + KEY_LEN:
|
if not len(pwhash) == 1 + SALT_LEN + KEY_LEN:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Invalid hash size, {len(pwhash)} != {1 + SALT_LEN + KEY_LEN}")
|
f"Invalid hash size, {len(pwhash)} != {1 + SALT_LEN + KEY_LEN}"
|
||||||
|
)
|
||||||
|
|
||||||
if (ver := pwhash[0:1]) != VERSION:
|
if (ver := pwhash[0:1]) != VERSION:
|
||||||
raise Exception(f"Invalid hash version, {ver!r} != {VERSION!r}")
|
raise Exception(f"Invalid hash version, {ver!r} != {VERSION!r}")
|
||||||
|
@ -9,9 +9,11 @@ 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
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
|
||||||
def create_tls_context(certfile, keyfile) -> ssl.SSLContext:
|
def create_tls_context(certfile, keyfile) -> ssl.SSLContext:
|
||||||
@ -21,28 +23,31 @@ 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 = (
|
||||||
if cfg.logfile == "STDOUT":
|
"%(asctime)s %(name)s %(levelname)s %(message)s @ %(filename)s:%(lineno)d"
|
||||||
|
)
|
||||||
|
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: Optional[ssl.SSLContext] = 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: Union[config.TLSCfg, str]):
|
||||||
if tls == "default":
|
if tls == "default":
|
||||||
return default_tls_context
|
return default_tls_context
|
||||||
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,8 +59,13 @@ 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!")
|
||||||
|
return
|
||||||
|
|
||||||
|
for scfg in cfg.servers:
|
||||||
|
if scfg.server_type == "pop":
|
||||||
|
pop = config.PopCfg(scfg)
|
||||||
pop_server = await create_pop_server(
|
pop_server = await create_pop_server(
|
||||||
host=get_host(pop.host),
|
host=get_host(pop.host),
|
||||||
port=pop.port,
|
port=pop.port,
|
||||||
@ -65,9 +75,8 @@ async def a_main(cfg: config.Config) -> None:
|
|||||||
timeout_seconds=pop.timeout_seconds,
|
timeout_seconds=pop.timeout_seconds,
|
||||||
)
|
)
|
||||||
servers.append(pop_server)
|
servers.append(pop_server)
|
||||||
|
elif scfg.server_type == "smtp_starttls":
|
||||||
if cfg.smtp_starttls:
|
stls = config.SmtpStartTLSCfg(scfg)
|
||||||
stls = config.SmtpStartTLSCfg(cfg.smtp_starttls)
|
|
||||||
stls_context = get_tls_context(stls.tls)
|
stls_context = get_tls_context(stls.tls)
|
||||||
if not stls_context:
|
if not stls_context:
|
||||||
raise Exception("starttls requires ssl_context")
|
raise Exception("starttls requires ssl_context")
|
||||||
@ -79,9 +88,8 @@ async def a_main(cfg: config.Config) -> None:
|
|||||||
ssl_context=stls_context,
|
ssl_context=stls_context,
|
||||||
)
|
)
|
||||||
servers.append(smtp_server_starttls)
|
servers.append(smtp_server_starttls)
|
||||||
|
elif scfg.server_type == "smtp":
|
||||||
if cfg.smtp:
|
smtp = config.SmtpCfg(scfg)
|
||||||
smtp = config.SmtpCfg(cfg.smtp)
|
|
||||||
smtp_server = await create_smtp_server(
|
smtp_server = await create_smtp_server(
|
||||||
host=get_host(smtp.host),
|
host=get_host(smtp.host),
|
||||||
port=smtp.port,
|
port=smtp.port,
|
||||||
@ -90,11 +98,13 @@ async def a_main(cfg: config.Config) -> None:
|
|||||||
ssl_context=get_tls_context(smtp.tls),
|
ssl_context=get_tls_context(smtp.tls),
|
||||||
)
|
)
|
||||||
servers.append(smtp_server)
|
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 +112,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 +161,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))
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import uuid
|
|||||||
import shutil
|
import shutil
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
from . import config
|
from . import config
|
||||||
from email.message import Message
|
from email.message import Message
|
||||||
import email.policy
|
import email.policy
|
||||||
@ -25,15 +25,14 @@ logger = logging.getLogger("smtp")
|
|||||||
|
|
||||||
|
|
||||||
class MyHandler(AsyncMessage):
|
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__()
|
super().__init__()
|
||||||
self.mails_path = mails_path
|
self.mails_path = mails_path
|
||||||
self.mbox_finder = mbox_finder
|
self.mbox_finder = mbox_finder
|
||||||
|
|
||||||
async def handle_DATA(self, server: SMTPServer, session: SMTPSession,
|
async def handle_DATA(
|
||||||
envelope: SMTPEnvelope) -> str:
|
self, server: SMTPServer, session: SMTPSession, envelope: SMTPEnvelope
|
||||||
|
) -> str:
|
||||||
self.rcpt_tos = envelope.rcpt_tos
|
self.rcpt_tos = envelope.rcpt_tos
|
||||||
self.peer = session.peer
|
self.peer = session.peer
|
||||||
return await super().handle_DATA(server, session, envelope)
|
return await super().handle_DATA(server, session, envelope)
|
||||||
@ -63,9 +62,9 @@ class MyHandler(AsyncMessage):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def protocol_factory_starttls(mails_path: Path,
|
def protocol_factory_starttls(
|
||||||
mbox_finder: Callable[[str], list[str]],
|
mails_path: Path, mbox_finder: Callable[[str], list[str]], context: ssl.SSLContext
|
||||||
context: ssl.SSLContext):
|
):
|
||||||
logger.info("Got smtp client cb starttls")
|
logger.info("Got smtp client cb starttls")
|
||||||
try:
|
try:
|
||||||
handler = MyHandler(mails_path, mbox_finder)
|
handler = MyHandler(mails_path, mbox_finder)
|
||||||
@ -75,20 +74,19 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def protocol_factory(mails_path: Path, mbox_finder: Callable[[str],
|
def protocol_factory(mails_path: Path, mbox_finder: Callable[[str], list[str]]):
|
||||||
list[str]]):
|
|
||||||
logger.info("Got smtp client cb")
|
logger.info("Got smtp client cb")
|
||||||
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,12 +99,11 @@ 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(
|
||||||
partial(protocol_factory_starttls, mails_path, mbox_finder,
|
partial(protocol_factory_starttls, mails_path, mbox_finder, ssl_context),
|
||||||
ssl_context),
|
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
start_serving=False,
|
start_serving=False,
|
||||||
@ -118,10 +115,10 @@ async def create_smtp_server(
|
|||||||
port: int,
|
port: int,
|
||||||
mails_path: Path,
|
mails_path: Path,
|
||||||
mbox_finder: Callable[[str], list[str]],
|
mbox_finder: Callable[[str], list[str]],
|
||||||
ssl_context: ssl.SSLContext | None = None,
|
ssl_context: Optional[ssl.SSLContext] = 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(
|
||||||
|
1
mail4one/version.py
Normal file
1
mail4one/version.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
VERSION = "DEVELOMENT"
|
29
scripts/get_version.sh
Executable file
29
scripts/get_version.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/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"
|
||||||
|
;;
|
||||||
|
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
|
||||||
|
|
Reference in New Issue
Block a user