diff --git a/.gitignore b/.gitignore index 3c4308c..79e204d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ dummy.py build mail4one.pyz +deploy_configs/config.json diff --git a/Makefile b/Makefile index d1d8772..60d3459 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,5 @@ -shell: - 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 - docker run --pull=always -v `pwd`:/app -w /app --rm -it python:3.9 sh runtests.sh - -requirements.txt: Pipfile.lock - pipenv requirements > requirements.txt - -build: clean requirements.txt +# Needs python3 >= 3.9, sed, git for build +build: clean 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 @@ -30,5 +15,31 @@ clean: rm -rf build rm -rf mail4one.pyz +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 + docker run --pull=always -v `pwd`:/app -w /app --rm -it python:3.9 sh 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 diff --git a/README.md b/README.md index 4f225f4..23ee427 100644 --- a/README.md +++ b/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 -* pop3 server with TLS -* Both running on single thread using asyncio -* Saves mails in simple Maildir format (i.e one file per email message) -* After opening port, drops root privileges. So the process will not running as `nobody` + 1. Get a domain name + 1. Get a VPS (or a home server). Setup firewall rules for receive on port 25, 995, 465 + 1. Setup [MX record](#dns-records-receiving) + 1. [Build](#building-from-source) / Download latest release - `mail4one.pyz` + 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+" | sha256sum - 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 +Mail4one only takes care of receiving and serving email. For sending email, use an external service like below -## 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 - * 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. +# Community -## Goals - * 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 +Original source is at https://gitea.balki.me/balki/mail4one -## Known to work - * 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 +For issues, pull requests, discussions, please use github mirror: https://github.com/mail4one/mail4one -## 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 diff --git a/deploy_configs/config.sample b/deploy_configs/config.sample new file mode 100644 index 0000000..bc60287 --- /dev/null +++ b/deploy_configs/config.sample @@ -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/ + # 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 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 //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 diff --git a/deploy_configs/mail4one.conf b/deploy_configs/mail4one.conf index cd6878f..56f585c 100644 --- a/deploy_configs/mail4one.conf +++ b/deploy_configs/mail4one.conf @@ -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. u mail4one - "Personal Mail server" diff --git a/deploy_configs/mail4one.service b/deploy_configs/mail4one.service index 9401a71..ff64c65 100644 --- a/deploy_configs/mail4one.service +++ b/deploy_configs/mail4one.service @@ -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] Description=Personal Mail server @@ -7,12 +10,14 @@ After=network.target network-online.target Requires=network-online.target [Service] + +# This user should already exist. See mail4one.conf for creating user with sysusers User=mail4one ExecStart=/usr/local/bin/mail4one --config /etc/mail4one/config.json +# Below allows to bind to port < 1024. Standard ports are 25, 465, 995 AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE -NoNewPrivileges=yes StateDirectory=mail4one/certs mail4one/mails StateDirectoryMode=0750 @@ -23,6 +28,7 @@ ProtectSystem=strict PrivateTmp=true ProtectHome=yes ProtectProc=invisible +NoNewPrivileges=yes [Install] WantedBy=multi-user.target diff --git a/deploy_configs/mail4one_cert_copy.sh b/deploy_configs/mail4one_cert_copy.sh index 8e27527..de263a4 100755 --- a/deploy_configs/mail4one_cert_copy.sh +++ b/deploy_configs/mail4one_cert_copy.sh @@ -1,6 +1,8 @@ #!/bin/sh # 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/ # Change the mail domain to the one on MX record