mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 08:24:37 +00:00
Compare commits
73 Commits
docker-dns
...
docker-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1fff288b | ||
|
|
b070677362 | ||
|
|
00342dd667 | ||
|
|
a23e8a8e59 | ||
|
|
2f4cfcc03d | ||
|
|
8ffb97a538 | ||
|
|
ad03bdb80d | ||
|
|
28bf01912a | ||
|
|
209a3cc272 | ||
|
|
884bd9570b | ||
|
|
07010c27e6 | ||
|
|
e102f1ace2 | ||
|
|
1c4e118986 | ||
|
|
ae3214f45e | ||
|
|
4463c62ba7 | ||
|
|
2136469f02 | ||
|
|
5952465690 | ||
|
|
29b8bb34ee | ||
|
|
e7ddf6dc32 | ||
|
|
e3c77a5b37 | ||
|
|
8256080ad1 | ||
|
|
248b225665 | ||
|
|
79591adca4 | ||
|
|
185757cf40 | ||
|
|
87a3adec03 | ||
|
|
4f5719f590 | ||
|
|
9787b63cbb | ||
|
|
6f600fa329 | ||
|
|
20b6e0c528 | ||
|
|
262e98f0ba | ||
|
|
d720b8107d | ||
|
|
d7f50183ea | ||
|
|
248603ab0a | ||
|
|
123531f1eb | ||
|
|
1170adc1d4 | ||
|
|
a6f7ff3652 | ||
|
|
d39076f0d6 | ||
|
|
65c0bf13f2 | ||
|
|
0ed7c360a9 | ||
|
|
af272545dd | ||
|
|
7725a73cf5 | ||
|
|
e65311c0df | ||
|
|
d091b865c7 | ||
|
|
6e28cf9ca1 | ||
|
|
9b6dfa9cdc | ||
|
|
44ab006dca | ||
|
|
c56805211f | ||
|
|
05ec64bf4a | ||
|
|
290e80e795 | ||
|
|
56fab1b071 | ||
|
|
00ab53800e | ||
|
|
fc65072edb | ||
|
|
7bf2dfd62e | ||
|
|
b801838b69 | ||
|
|
abd50e20ed | ||
|
|
d6fb38750a | ||
|
|
3b73457de3 | ||
|
|
ba06a4ff70 | ||
|
|
7fdaffe829 | ||
|
|
73831c74d9 | ||
|
|
d8cbe9d6af | ||
|
|
180ddb8168 | ||
|
|
a1eeea4632 | ||
|
|
a49aa0e655 | ||
|
|
7e81495b51 | ||
|
|
6fde062613 | ||
|
|
84e0376762 | ||
|
|
d690c22c06 | ||
|
|
5410c1bebc | ||
|
|
915bd39dd5 | ||
|
|
2de8b155c2 | ||
|
|
c975aa3bd1 | ||
|
|
6b73f6933a |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Mutual Help Chat Group
|
- name: Mutual Help Chat Group
|
||||||
url: https://i.delta.chat/#C2846EB4C1CB8DF84B1818F5E3A638FC3FBDC981&a=stalebot1%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=d7s1HvOsk5UrSf9AoqRZggg4&s=XmX_9BAW6-g5Ao5E8PyaeKNB
|
url: https://i.delta.chat/#6CBFF8FFD505C0FDEA20A66674F2916EA8FBEE99&a=invitebot%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=X69wTFfvCfs3d-JzqP0kVA3i&s=ibp-447dU-wUq-52QanwAtWc
|
||||||
about: If you have troubles setting up the relay server, feel free to ask here.
|
about: If you have troubles setting up the relay server, feel free to ask here.
|
||||||
|
|||||||
@@ -70,9 +70,6 @@ jobs:
|
|||||||
rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true
|
rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true
|
||||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true
|
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true
|
||||||
|
|
||||||
- name: run formatting checks
|
|
||||||
run: cmdeploy fmt -v
|
|
||||||
|
|
||||||
- name: run deploy-chatmail offline tests
|
- name: run deploy-chatmail offline tests
|
||||||
run: pytest --pyargs cmdeploy
|
run: pytest --pyargs cmdeploy
|
||||||
|
|
||||||
@@ -80,7 +77,7 @@ jobs:
|
|||||||
cmdeploy init staging-ipv4.testrun.org
|
cmdeploy init staging-ipv4.testrun.org
|
||||||
sed -i 's#disable_ipv6 = False#disable_ipv6 = True#' chatmail.ini
|
sed -i 's#disable_ipv6 = False#disable_ipv6 = True#' chatmail.ini
|
||||||
|
|
||||||
- run: cmdeploy run
|
- run: cmdeploy run --verbose --skip-dns-check
|
||||||
|
|
||||||
- name: set DNS entries
|
- name: set DNS entries
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
5
.github/workflows/test-and-deploy.yaml
vendored
5
.github/workflows/test-and-deploy.yaml
vendored
@@ -70,15 +70,12 @@ jobs:
|
|||||||
rsync -avz dkimkeys-restore/dkimkeys root@staging2.testrun.org:/etc/ || true
|
rsync -avz dkimkeys-restore/dkimkeys root@staging2.testrun.org:/etc/ || true
|
||||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org chown root:root -R /var/lib/acme || true
|
ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org chown root:root -R /var/lib/acme || true
|
||||||
|
|
||||||
- name: run formatting checks
|
|
||||||
run: cmdeploy fmt -v
|
|
||||||
|
|
||||||
- name: run deploy-chatmail offline tests
|
- name: run deploy-chatmail offline tests
|
||||||
run: pytest --pyargs cmdeploy
|
run: pytest --pyargs cmdeploy
|
||||||
|
|
||||||
- run: cmdeploy init staging2.testrun.org
|
- run: cmdeploy init staging2.testrun.org
|
||||||
|
|
||||||
- run: cmdeploy run --verbose
|
- run: cmdeploy run --verbose --skip-dns-check
|
||||||
|
|
||||||
- name: set DNS entries
|
- name: set DNS entries
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -170,4 +170,3 @@ chatmail.zone
|
|||||||
/custom/
|
/custom/
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
.env
|
.env
|
||||||
/traefik/data/
|
|
||||||
|
|||||||
50
ARCHITECTURE.md
Normal file
50
ARCHITECTURE.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
This diagram shows components of the chatmail server; this is a draft
|
||||||
|
overview as of mid-August 2025:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR;
|
||||||
|
cmdeploy --- sshd;
|
||||||
|
letsencrypt --- |80|acmetool-redirector;
|
||||||
|
acmetool-redirector --- |443|nginx-right(["`nginx
|
||||||
|
(external)`"]);
|
||||||
|
nginx-external --- |465|postfix;
|
||||||
|
nginx-external(["`nginx
|
||||||
|
(external)`"]) --- |8443|nginx-internal["`nginx
|
||||||
|
(internal)`"];
|
||||||
|
nginx-internal --- website["`Website
|
||||||
|
/var/www/html`"];
|
||||||
|
nginx-internal --- newemail.py;
|
||||||
|
nginx-internal --- autoconfig.xml;
|
||||||
|
certs-nginx[("`TLS certs
|
||||||
|
/var/lib/acme`")] --> nginx-internal;
|
||||||
|
cron --- chatmail-metrics;
|
||||||
|
cron --- acmetool;
|
||||||
|
cron --- expunge;
|
||||||
|
chatmail-metrics --- website;
|
||||||
|
acmetool --> certs[("`TLS certs
|
||||||
|
/var/lib/acme`")];
|
||||||
|
nginx-external --- |993|dovecot;
|
||||||
|
autoconfig.xml --- postfix;
|
||||||
|
autoconfig.xml --- dovecot;
|
||||||
|
postfix --- echobot;
|
||||||
|
postfix --- |10080,10081|filtermail;
|
||||||
|
postfix --- users["`User data
|
||||||
|
home/vmail/mail`"];
|
||||||
|
postfix --- |doveauth.socket|doveauth;
|
||||||
|
dovecot --- |doveauth.socket|doveauth;
|
||||||
|
dovecot --- users;
|
||||||
|
dovecot --- |metadata.socket|chatmail-metadata;
|
||||||
|
doveauth --- users;
|
||||||
|
expunge --- users;
|
||||||
|
chatmail-metadata --- iroh-relay;
|
||||||
|
certs-nginx --> postfix;
|
||||||
|
certs-nginx --> dovecot;
|
||||||
|
style certs fill:#ff6;
|
||||||
|
style certs-nginx fill:#ff6;
|
||||||
|
style nginx-external fill:#fc9;
|
||||||
|
style nginx-right fill:#fc9;
|
||||||
|
```
|
||||||
|
|
||||||
|
The edges in this graph should not be taken too literally; they
|
||||||
|
reflect some sort of communication path or dependency relationship
|
||||||
|
between components of the chatmail server.
|
||||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -2,35 +2,55 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
- Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs`
|
- Setup TURN server
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#621](https://github.com/chatmail/relay/pull/621))
|
||||||
|
|
||||||
- Add markdown tabs blocks for rendering multilingual pages. Add russian language support to `index.md`, `privacy.md`, and `info.md`.
|
- cmdeploy: make --ssh-host work with localhost
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#659](https://github.com/chatmail/relay/pull/659))
|
||||||
|
|
||||||
- Fix [Issue 604](https://github.com/chatmail/relay/issues/604), now the `--ssh_host` argument of the `cmdeploy run` command works correctly and does not depend on `config.mail_domain`.
|
- Update iroh-relay to 0.35.0
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#650](https://github.com/chatmail/relay/pull/650))
|
||||||
|
|
||||||
- Add `--skip-dns-check` argument to `cmdeploy run` command, which disables DNS record checking before installation.
|
- filtermail: accept mails from Protonmail
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#616](https://github.com/chatmail/relay/pull/655))
|
||||||
|
|
||||||
- Add `--force` argument to `cmdeploy init` command, which recreates the `chatmail.ini` file.
|
- Ignore all RCPT TO: parameters
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#651](https://github.com/chatmail/relay/pull/651))
|
||||||
|
|
||||||
|
- Add config parameter for Let's Encrypt ACME email
|
||||||
|
([#663](https://github.com/chatmail/relay/pull/663))
|
||||||
|
|
||||||
|
- Use max username length in newemail.py, not min
|
||||||
|
([#648](https://github.com/chatmail/relay/pull/648))
|
||||||
|
|
||||||
- Add startup for `fcgiwrap.service` because sometimes it did not start automatically.
|
- Add startup for `fcgiwrap.service` because sometimes it did not start automatically.
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#657](https://github.com/chatmail/relay/pull/657))
|
||||||
|
|
||||||
- Add extended check when installing `unbound.service`. Now, if it is not shown who exactly is occupying port 53, but `unbound.service` is running, it is considered that the port is occupied by `unbound.service`.
|
- Add `cmdeploy init --force` command for recreating chatmail.ini
|
||||||
|
([#656](https://github.com/chatmail/relay/pull/656))
|
||||||
|
|
||||||
|
- Increase maxproc for reinjecting ports from 10 to 100
|
||||||
|
([#646](https://github.com/chatmail/relay/pull/646))
|
||||||
|
|
||||||
|
- Allow ports 143 and 993 to be used by `dovecot` process
|
||||||
|
([#639](https://github.com/chatmail/relay/pull/639))
|
||||||
|
|
||||||
|
- Add `--skip-dns-check` argument to `cmdeploy run` command, which disables DNS record checking before installation.
|
||||||
|
([#661](https://github.com/chatmail/relay/pull/661))
|
||||||
|
|
||||||
|
- Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs`
|
||||||
([#614](https://github.com/chatmail/relay/pull/614))
|
([#614](https://github.com/chatmail/relay/pull/614))
|
||||||
|
|
||||||
- Add configuration parameters
|
- Add configuration parameters
|
||||||
([#614](https://github.com/chatmail/relay/pull/614)):
|
([#614](https://github.com/chatmail/relay/pull/614)):
|
||||||
- `is_development_instance` - Indicates that this instance is installed as a temporary/test one (default: `True`)
|
|
||||||
- `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`)
|
|
||||||
- `acme_email` - Email address used by acmetool to obtain Let's Encrypt certificates (default: empty)
|
|
||||||
- `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`)
|
- `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`)
|
||||||
- `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`)
|
- `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`)
|
||||||
|
|
||||||
|
## 1.7.0 2025-09-11
|
||||||
|
|
||||||
|
- Make www upload path configurable
|
||||||
|
([#618](https://github.com/chatmail/relay/pull/618))
|
||||||
|
|
||||||
- Check whether GCC is installed in initenv.sh
|
- Check whether GCC is installed in initenv.sh
|
||||||
([#608](https://github.com/chatmail/relay/pull/608))
|
([#608](https://github.com/chatmail/relay/pull/608))
|
||||||
|
|
||||||
@@ -58,6 +78,9 @@
|
|||||||
- filtermail: respect config message size limit
|
- filtermail: respect config message size limit
|
||||||
([#572](https://github.com/chatmail/relay/pull/572))
|
([#572](https://github.com/chatmail/relay/pull/572))
|
||||||
|
|
||||||
|
- Don't deploy if one of the ports used for chatmail relay services is occupied by an unexpected process
|
||||||
|
([#568](https://github.com/chatmail/relay/pull/568))
|
||||||
|
|
||||||
- Add config value after how many days large files are deleted
|
- Add config value after how many days large files are deleted
|
||||||
([#555](https://github.com/chatmail/relay/pull/555))
|
([#555](https://github.com/chatmail/relay/pull/555))
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -101,7 +101,10 @@ Please substitute it with your own domain.
|
|||||||
(it can take some time until they are public).
|
(it can take some time until they are public).
|
||||||
|
|
||||||
### Docker installation
|
### Docker installation
|
||||||
Installation using docker compose is presented [here](./docs/DOCKER_INSTALLATION_EN.md)
|
|
||||||
|
We have experimental support for [docker compose](./docs/DOCKER_INSTALLATION_EN.md),
|
||||||
|
but it is not covered by automated tests yet,
|
||||||
|
so don't expect everything to work.
|
||||||
|
|
||||||
### Other helpful commands
|
### Other helpful commands
|
||||||
|
|
||||||
@@ -259,6 +262,18 @@ This starts a local live development cycle for chatmail web pages:
|
|||||||
|
|
||||||
- Starts a browser window automatically where you can "refresh" as needed.
|
- Starts a browser window automatically where you can "refresh" as needed.
|
||||||
|
|
||||||
|
#### Custom web pages
|
||||||
|
|
||||||
|
You can skip uploading a web page
|
||||||
|
by setting `www_folder=disabled` in `chatmail.ini`.
|
||||||
|
|
||||||
|
If you want to manage your web pages outside this git repository,
|
||||||
|
you can set `www_folder` in `chatmail.ini` to a custom directory on your computer.
|
||||||
|
`cmdeploy run` will upload it as the server's home page,
|
||||||
|
and if it contains a `src/index.md` file,
|
||||||
|
will build it with hugo.
|
||||||
|
|
||||||
|
|
||||||
## Mailbox directory layout
|
## Mailbox directory layout
|
||||||
|
|
||||||
Fresh chatmail addresses have a mailbox directory that contains:
|
Fresh chatmail addresses have a mailbox directory that contains:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ echobot = "chatmaild.echo:main"
|
|||||||
chatmail-metrics = "chatmaild.metrics:main"
|
chatmail-metrics = "chatmaild.metrics:main"
|
||||||
delete_inactive_users = "chatmaild.delete_inactive_users:main"
|
delete_inactive_users = "chatmaild.delete_inactive_users:main"
|
||||||
lastlogin = "chatmaild.lastlogin:main"
|
lastlogin = "chatmaild.lastlogin:main"
|
||||||
|
turnserver = "chatmaild.turnserver:main"
|
||||||
|
|
||||||
[project.entry-points.pytest11]
|
[project.entry-points.pytest11]
|
||||||
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ class Config:
|
|||||||
self.password_min_length = int(params["password_min_length"])
|
self.password_min_length = int(params["password_min_length"])
|
||||||
self.passthrough_senders = params["passthrough_senders"].split()
|
self.passthrough_senders = params["passthrough_senders"].split()
|
||||||
self.passthrough_recipients = params["passthrough_recipients"].split()
|
self.passthrough_recipients = params["passthrough_recipients"].split()
|
||||||
self.is_development_instance = (
|
self.www_folder = params.get("www_folder", "")
|
||||||
params.get("is_development_instance", "true").lower() == "true"
|
|
||||||
)
|
|
||||||
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
||||||
self.filtermail_smtp_port_incoming = int(
|
self.filtermail_smtp_port_incoming = int(
|
||||||
params["filtermail_smtp_port_incoming"]
|
params["filtermail_smtp_port_incoming"]
|
||||||
@@ -46,10 +44,7 @@ class Config:
|
|||||||
)
|
)
|
||||||
self.mtail_address = params.get("mtail_address")
|
self.mtail_address = params.get("mtail_address")
|
||||||
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
|
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
|
||||||
self.use_foreign_cert_manager = (
|
self.acme_email = params.get("acme_email", "")
|
||||||
params.get("use_foreign_cert_manager", "false").lower() == "true"
|
|
||||||
)
|
|
||||||
self.acme_email = params["acme_email"]
|
|
||||||
self.change_kernel_settings = (
|
self.change_kernel_settings = (
|
||||||
params.get("change_kernel_settings", "true").lower() == "true"
|
params.get("change_kernel_settings", "true").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -83,8 +83,14 @@ def check_openpgp_payload(payload: bytes):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_armored_payload(payload: str):
|
def check_armored_payload(payload: str, outgoing: bool):
|
||||||
prefix = "-----BEGIN PGP MESSAGE-----\r\n\r\n"
|
"""Check the armored PGP message for invalid content.
|
||||||
|
|
||||||
|
:param payload: the armored PGP message
|
||||||
|
:param outgoing: whether the message is outgoing or incoming
|
||||||
|
:return: whether the message is a valid PGP message
|
||||||
|
"""
|
||||||
|
prefix = "-----BEGIN PGP MESSAGE-----\r\n"
|
||||||
if not payload.startswith(prefix):
|
if not payload.startswith(prefix):
|
||||||
return False
|
return False
|
||||||
payload = payload.removeprefix(prefix)
|
payload = payload.removeprefix(prefix)
|
||||||
@@ -96,6 +102,17 @@ def check_armored_payload(payload: str):
|
|||||||
return False
|
return False
|
||||||
payload = payload.removesuffix(suffix)
|
payload = payload.removesuffix(suffix)
|
||||||
|
|
||||||
|
# Disallow comments in outgoing messages
|
||||||
|
version_comment = "Version: "
|
||||||
|
if payload.startswith(version_comment):
|
||||||
|
version_line = payload.splitlines()[0]
|
||||||
|
payload = payload.removeprefix(version_line)
|
||||||
|
if outgoing:
|
||||||
|
return False
|
||||||
|
|
||||||
|
while payload.startswith("\r\n"):
|
||||||
|
payload = payload.removeprefix("\r\n")
|
||||||
|
|
||||||
# Remove CRC24.
|
# Remove CRC24.
|
||||||
payload = payload.rpartition("=")[0]
|
payload = payload.rpartition("=")[0]
|
||||||
|
|
||||||
@@ -131,7 +148,7 @@ def is_securejoin(message):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_encrypted(message):
|
def check_encrypted(message, outgoing=True):
|
||||||
"""Check that the message is an OpenPGP-encrypted message.
|
"""Check that the message is an OpenPGP-encrypted message.
|
||||||
|
|
||||||
MIME structure of the message must correspond to <https://www.rfc-editor.org/rfc/rfc3156>.
|
MIME structure of the message must correspond to <https://www.rfc-editor.org/rfc/rfc3156>.
|
||||||
@@ -158,7 +175,7 @@ def check_encrypted(message):
|
|||||||
if part.get_content_type() != "application/octet-stream":
|
if part.get_content_type() != "application/octet-stream":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not check_armored_payload(part.get_payload()):
|
if not check_armored_payload(part.get_payload(), outgoing=outgoing):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -197,11 +214,13 @@ class HackedController(Controller):
|
|||||||
|
|
||||||
class SMTPDiscardRCPTO_options(SMTP):
|
class SMTPDiscardRCPTO_options(SMTP):
|
||||||
def _getparams(self, params):
|
def _getparams(self, params):
|
||||||
# aiosmtpd's SMTP daemon fails to handle a request if there are RCPT TO options
|
# Ignore RCPT TO parameters.
|
||||||
# We just ignore them for our incoming filtermail purposes
|
#
|
||||||
if len(params) == 1 and params[0].startswith("ORCPT"):
|
# Otherwise parameters such as `ORCPT=...`
|
||||||
return {}
|
# or `NOTIFY=DELAY,FAILURE` (generated by Stalwart)
|
||||||
return super()._getparams(params)
|
# make aiosmtpd reject the message here:
|
||||||
|
# <https://github.com/aio-libs/aiosmtpd/blob/98f578389ae86e5345cc343fa4e5a17b21d9c96d/aiosmtpd/smtp.py#L1379-L1384>
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class OutgoingBeforeQueueHandler:
|
class OutgoingBeforeQueueHandler:
|
||||||
@@ -239,7 +258,7 @@ class OutgoingBeforeQueueHandler:
|
|||||||
logging.info(f"Processing DATA message from {envelope.mail_from}")
|
logging.info(f"Processing DATA message from {envelope.mail_from}")
|
||||||
|
|
||||||
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
|
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
|
||||||
mail_encrypted = check_encrypted(message)
|
mail_encrypted = check_encrypted(message, outgoing=True)
|
||||||
|
|
||||||
_, from_addr = parseaddr(message.get("from").strip())
|
_, from_addr = parseaddr(message.get("from").strip())
|
||||||
|
|
||||||
@@ -299,7 +318,7 @@ class IncomingBeforeQueueHandler:
|
|||||||
logging.info(f"Processing DATA message from {envelope.mail_from}")
|
logging.info(f"Processing DATA message from {envelope.mail_from}")
|
||||||
|
|
||||||
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
|
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
|
||||||
mail_encrypted = check_encrypted(message)
|
mail_encrypted = check_encrypted(message, outgoing=False)
|
||||||
|
|
||||||
if mail_encrypted or is_securejoin(message):
|
if mail_encrypted or is_securejoin(message):
|
||||||
print("Incoming: Filtering encrypted mail.", file=sys.stderr)
|
print("Incoming: Filtering encrypted mail.", file=sys.stderr)
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ passthrough_senders =
|
|||||||
# (space-separated, item may start with "@" to whitelist whole recipient domains)
|
# (space-separated, item may start with "@" to whitelist whole recipient domains)
|
||||||
passthrough_recipients = xstore@testrun.org echo@{mail_domain}
|
passthrough_recipients = xstore@testrun.org echo@{mail_domain}
|
||||||
|
|
||||||
|
# path to www directory - documented here: https://github.com/chatmail/relay/#custom-web-pages
|
||||||
|
#www_folder = www
|
||||||
|
|
||||||
#
|
#
|
||||||
# Deployment Details
|
# Deployment Details
|
||||||
#
|
#
|
||||||
|
|
||||||
# set to "False" to remove the "development instance" banner on the main page.
|
|
||||||
is_development_instance = True
|
|
||||||
|
|
||||||
# SMTP outgoing filtermail and reinjection
|
# SMTP outgoing filtermail and reinjection
|
||||||
filtermail_smtp_port = 10080
|
filtermail_smtp_port = 10080
|
||||||
postfix_reinject_port = 10025
|
postfix_reinject_port = 10025
|
||||||
@@ -63,10 +63,7 @@ postfix_reinject_port_incoming = 10026
|
|||||||
# if set to "True" IPv6 is disabled
|
# if set to "True" IPv6 is disabled
|
||||||
disable_ipv6 = False
|
disable_ipv6 = False
|
||||||
|
|
||||||
# if you set "True", acmetool will not be installed and you will have to manage certificates yourself.
|
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates
|
||||||
use_foreign_cert_manager = False
|
|
||||||
|
|
||||||
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates. Required if `use_foreign_cert_manager` param set as "False".
|
|
||||||
acme_email =
|
acme_email =
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .config import read_config
|
|||||||
from .dictproxy import DictProxy
|
from .dictproxy import DictProxy
|
||||||
from .filedict import FileDict
|
from .filedict import FileDict
|
||||||
from .notifier import Notifier
|
from .notifier import Notifier
|
||||||
|
from .turnserver import turn_credentials
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_token_timestamp(timestamp, now):
|
def _is_valid_token_timestamp(timestamp, now):
|
||||||
@@ -75,11 +76,12 @@ class Metadata:
|
|||||||
|
|
||||||
|
|
||||||
class MetadataDictProxy(DictProxy):
|
class MetadataDictProxy(DictProxy):
|
||||||
def __init__(self, notifier, metadata, iroh_relay=None):
|
def __init__(self, notifier, metadata, iroh_relay=None, turn_hostname=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.notifier = notifier
|
self.notifier = notifier
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.iroh_relay = iroh_relay
|
self.iroh_relay = iroh_relay
|
||||||
|
self.turn_hostname = turn_hostname
|
||||||
|
|
||||||
def handle_lookup(self, parts):
|
def handle_lookup(self, parts):
|
||||||
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
||||||
@@ -98,6 +100,11 @@ class MetadataDictProxy(DictProxy):
|
|||||||
):
|
):
|
||||||
# Handle `GETMETADATA "" /shared/vendor/deltachat/irohrelay`
|
# Handle `GETMETADATA "" /shared/vendor/deltachat/irohrelay`
|
||||||
return f"O{self.iroh_relay}\n"
|
return f"O{self.iroh_relay}\n"
|
||||||
|
elif keyname == "vendor/vendor.dovecot/pvt/server/vendor/deltachat/turn":
|
||||||
|
res = turn_credentials()
|
||||||
|
port = 3478
|
||||||
|
return f"O{self.turn_hostname}:{port}:{res}\n"
|
||||||
|
|
||||||
logging.warning(f"lookup ignored: {parts!r}")
|
logging.warning(f"lookup ignored: {parts!r}")
|
||||||
return "N\n"
|
return "N\n"
|
||||||
|
|
||||||
@@ -121,6 +128,7 @@ def main():
|
|||||||
|
|
||||||
config = read_config(config_path)
|
config = read_config(config_path)
|
||||||
iroh_relay = config.iroh_relay
|
iroh_relay = config.iroh_relay
|
||||||
|
mail_domain = config.mail_domain
|
||||||
|
|
||||||
vmail_dir = config.mailboxes_dir
|
vmail_dir = config.mailboxes_dir
|
||||||
if not vmail_dir.exists():
|
if not vmail_dir.exists():
|
||||||
@@ -134,7 +142,10 @@ def main():
|
|||||||
notifier.start_notification_threads(metadata.remove_token_from_addr)
|
notifier.start_notification_threads(metadata.remove_token_from_addr)
|
||||||
|
|
||||||
dictproxy = MetadataDictProxy(
|
dictproxy = MetadataDictProxy(
|
||||||
notifier=notifier, metadata=metadata, iroh_relay=iroh_relay
|
notifier=notifier,
|
||||||
|
metadata=metadata,
|
||||||
|
iroh_relay=iroh_relay,
|
||||||
|
turn_hostname=mail_domain,
|
||||||
)
|
)
|
||||||
|
|
||||||
dictproxy.serve_forever_from_socket(socket)
|
dictproxy.serve_forever_from_socket(socket)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
|
|||||||
|
|
||||||
|
|
||||||
def create_newemail_dict(config: Config):
|
def create_newemail_dict(config: Config):
|
||||||
user = "".join(random.choices(ALPHANUMERIC, k=config.username_min_length))
|
user = "".join(random.choices(ALPHANUMERIC, k=config.username_max_length))
|
||||||
password = "".join(
|
password = "".join(
|
||||||
secrets.choice(ALPHANUMERIC_PUNCT)
|
secrets.choice(ALPHANUMERIC_PUNCT)
|
||||||
for _ in range(config.password_min_length + 3)
|
for _ in range(config.password_min_length + 3)
|
||||||
|
|||||||
@@ -241,8 +241,9 @@ def test_cleartext_passthrough_senders(gencreds, handler, maildata):
|
|||||||
|
|
||||||
|
|
||||||
def test_check_armored_payload():
|
def test_check_armored_payload():
|
||||||
payload = """-----BEGIN PGP MESSAGE-----\r
|
prefix = "-----BEGIN PGP MESSAGE-----\r\n"
|
||||||
\r
|
comment = "Version: ProtonMail\r\n"
|
||||||
|
payload = """\r
|
||||||
wU4DSqFx0d1yqAoSAQdAYkX/ZN/Az4B0k7X47zKyWrXxlDEdS3WOy0Yf2+GJTFgg\r
|
wU4DSqFx0d1yqAoSAQdAYkX/ZN/Az4B0k7X47zKyWrXxlDEdS3WOy0Yf2+GJTFgg\r
|
||||||
Zk5ql0mLG8Ze+ZifCS0XMO4otlemSyJ0K1ZPdFMGzUDBTgNqzkFabxXoXRIBB0AM\r
|
Zk5ql0mLG8Ze+ZifCS0XMO4otlemSyJ0K1ZPdFMGzUDBTgNqzkFabxXoXRIBB0AM\r
|
||||||
755wlX41X6Ay3KhnwBq7yEqSykVH6F3x11iHPKraLCAGZoaS8bKKNy/zg5slda1X\r
|
755wlX41X6Ay3KhnwBq7yEqSykVH6F3x11iHPKraLCAGZoaS8bKKNy/zg5slda1X\r
|
||||||
@@ -278,16 +279,25 @@ UN4fiB0KR9JyG2ayUdNJVkXZSZLnHyRgiaadlpUo16LVvw==\r
|
|||||||
\r
|
\r
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert check_armored_payload(payload) == True
|
commented_payload = prefix + comment + payload
|
||||||
|
assert check_armored_payload(commented_payload, outgoing=False) == True
|
||||||
|
assert check_armored_payload(commented_payload, outgoing=True) == False
|
||||||
|
|
||||||
|
payload = prefix + payload
|
||||||
|
assert check_armored_payload(payload, outgoing=False) == True
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == True
|
||||||
|
|
||||||
payload = payload.removesuffix("\r\n")
|
payload = payload.removesuffix("\r\n")
|
||||||
assert check_armored_payload(payload) == True
|
assert check_armored_payload(payload, outgoing=False) == True
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == True
|
||||||
|
|
||||||
payload = payload.removesuffix("\r\n")
|
payload = payload.removesuffix("\r\n")
|
||||||
assert check_armored_payload(payload) == True
|
assert check_armored_payload(payload, outgoing=False) == True
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == True
|
||||||
|
|
||||||
payload = payload.removesuffix("\r\n")
|
payload = payload.removesuffix("\r\n")
|
||||||
assert check_armored_payload(payload) == True
|
assert check_armored_payload(payload, outgoing=False) == True
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == True
|
||||||
|
|
||||||
payload = """-----BEGIN PGP MESSAGE-----\r
|
payload = """-----BEGIN PGP MESSAGE-----\r
|
||||||
\r
|
\r
|
||||||
@@ -295,7 +305,8 @@ HELLOWORLD
|
|||||||
-----END PGP MESSAGE-----\r
|
-----END PGP MESSAGE-----\r
|
||||||
\r
|
\r
|
||||||
"""
|
"""
|
||||||
assert check_armored_payload(payload) == False
|
assert check_armored_payload(payload, outgoing=False) == False
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == False
|
||||||
|
|
||||||
payload = """-----BEGIN PGP MESSAGE-----\r
|
payload = """-----BEGIN PGP MESSAGE-----\r
|
||||||
\r
|
\r
|
||||||
@@ -303,7 +314,8 @@ HELLOWORLD
|
|||||||
-----END PGP MESSAGE-----\r
|
-----END PGP MESSAGE-----\r
|
||||||
\r
|
\r
|
||||||
"""
|
"""
|
||||||
assert check_armored_payload(payload) == False
|
assert check_armored_payload(payload, outgoing=False) == False
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == False
|
||||||
|
|
||||||
# Test payload using partial body length
|
# Test payload using partial body length
|
||||||
# as generated by GopenPGP.
|
# as generated by GopenPGP.
|
||||||
@@ -345,4 +357,5 @@ myLbG7cJB787QjplEyVe2P/JBO6xYvbkJLf9Q+HaviTO25rugRSrYsoKMDfO8VlQ\r
|
|||||||
=6iHb\r
|
=6iHb\r
|
||||||
-----END PGP MESSAGE-----\r
|
-----END PGP MESSAGE-----\r
|
||||||
"""
|
"""
|
||||||
assert check_armored_payload(payload) == True
|
assert check_armored_payload(payload, outgoing=False) == True
|
||||||
|
assert check_armored_payload(payload, outgoing=True) == True
|
||||||
|
|||||||
9
chatmaild/src/chatmaild/turnserver.py
Normal file
9
chatmaild/src/chatmaild/turnserver.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
def turn_credentials() -> str:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
||||||
|
client_socket.connect("/run/chatmail-turn/turn.socket")
|
||||||
|
with client_socket.makefile("rb") as file:
|
||||||
|
return file.readline().decode("utf-8")
|
||||||
@@ -20,7 +20,6 @@ dependencies = [
|
|||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"execnet",
|
"execnet",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
"pymdown-extensions",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ from io import StringIO
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from chatmaild.config import Config, read_config
|
from chatmaild.config import Config, read_config
|
||||||
from pyinfra import facts, host
|
from pyinfra import facts, host, logger
|
||||||
from pyinfra.api import FactBase
|
from pyinfra.api import FactBase
|
||||||
from pyinfra.facts.files import File
|
from pyinfra.facts.files import File, Sha256File
|
||||||
from pyinfra.facts.server import Sysctl
|
from pyinfra.facts.server import Sysctl
|
||||||
from pyinfra.facts.systemd import SystemdEnabled, SystemdStatus
|
from pyinfra.facts.systemd import SystemdEnabled
|
||||||
from pyinfra.operations import apt, files, pip, server, systemd
|
from pyinfra.operations import apt, files, pip, server, systemd
|
||||||
|
|
||||||
from .acmetool import deploy_acmetool
|
from .acmetool import deploy_acmetool
|
||||||
@@ -128,6 +128,7 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
|||||||
"echobot",
|
"echobot",
|
||||||
"chatmail-metadata",
|
"chatmail-metadata",
|
||||||
"lastlogin",
|
"lastlogin",
|
||||||
|
"turnserver",
|
||||||
):
|
):
|
||||||
execpath = fn if fn != "filtermail-incoming" else "filtermail"
|
execpath = fn if fn != "filtermail-incoming" else "filtermail"
|
||||||
params = dict(
|
params = dict(
|
||||||
@@ -498,6 +499,56 @@ def check_config(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_turn_server(config):
|
||||||
|
(url, sha256sum) = {
|
||||||
|
"x86_64": (
|
||||||
|
"https://github.com/chatmail/chatmail-turn/releases/download/v0.3/chatmail-turn-x86_64-linux",
|
||||||
|
"841e527c15fdc2940b0469e206188ea8f0af48533be12ecb8098520f813d41e4",
|
||||||
|
),
|
||||||
|
"aarch64": (
|
||||||
|
"https://github.com/chatmail/chatmail-turn/releases/download/v0.3/chatmail-turn-aarch64-linux",
|
||||||
|
"a5fc2d06d937b56a34e098d2cd72a82d3e89967518d159bf246dc69b65e81b42",
|
||||||
|
),
|
||||||
|
}[host.get_fact(facts.server.Arch)]
|
||||||
|
|
||||||
|
need_restart = False
|
||||||
|
|
||||||
|
existing_sha256sum = host.get_fact(Sha256File, "/usr/local/bin/chatmail-turn")
|
||||||
|
if existing_sha256sum != sha256sum:
|
||||||
|
server.shell(
|
||||||
|
name="Download chatmail-turn",
|
||||||
|
commands=[
|
||||||
|
f"(curl -L {url} >/usr/local/bin/chatmail-turn.new && (echo '{sha256sum} /usr/local/bin/chatmail-turn.new' | sha256sum -c) && mv /usr/local/bin/chatmail-turn.new /usr/local/bin/chatmail-turn)",
|
||||||
|
"chmod 755 /usr/local/bin/chatmail-turn",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
need_restart = True
|
||||||
|
|
||||||
|
source_path = importlib.resources.files(__package__).joinpath(
|
||||||
|
"service", "turnserver.service.f"
|
||||||
|
)
|
||||||
|
content = source_path.read_text().format(mail_domain=config.mail_domain).encode()
|
||||||
|
|
||||||
|
systemd_unit = files.put(
|
||||||
|
name="Upload turnserver.service",
|
||||||
|
src=io.BytesIO(content),
|
||||||
|
dest="/etc/systemd/system/turnserver.service",
|
||||||
|
user="root",
|
||||||
|
group="root",
|
||||||
|
mode="644",
|
||||||
|
)
|
||||||
|
need_restart |= systemd_unit.changed
|
||||||
|
|
||||||
|
systemd.service(
|
||||||
|
name="Setup turnserver service",
|
||||||
|
service="turnserver.service",
|
||||||
|
running=True,
|
||||||
|
enabled=True,
|
||||||
|
restarted=need_restart,
|
||||||
|
daemon_reload=systemd_unit.changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def deploy_mtail(config):
|
def deploy_mtail(config):
|
||||||
# Uninstall mtail package, we are going to install a static binary.
|
# Uninstall mtail package, we are going to install a static binary.
|
||||||
apt.packages(name="Uninstall mtail", packages=["mtail"], present=False)
|
apt.packages(name="Uninstall mtail", packages=["mtail"], present=False)
|
||||||
@@ -556,12 +607,12 @@ def deploy_mtail(config):
|
|||||||
def deploy_iroh_relay(config) -> None:
|
def deploy_iroh_relay(config) -> None:
|
||||||
(url, sha256sum) = {
|
(url, sha256sum) = {
|
||||||
"x86_64": (
|
"x86_64": (
|
||||||
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-x86_64-unknown-linux-musl.tar.gz",
|
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-x86_64-unknown-linux-musl.tar.gz",
|
||||||
"2ffacf7c0622c26b67a5895ee8e07388769599f60e5f52a3bd40a3258db89b2c",
|
"45c81199dbd70f8c4c30fef7f3b9727ca6e3cea8f2831333eeaf8aa71bf0fac1",
|
||||||
),
|
),
|
||||||
"aarch64": (
|
"aarch64": (
|
||||||
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-aarch64-unknown-linux-musl.tar.gz",
|
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-aarch64-unknown-linux-musl.tar.gz",
|
||||||
"b915037bcc1ff1110cc9fcb5de4a17c00ff576fd2f568cd339b3b2d54c420dc4",
|
"f8ef27631fac213b3ef668d02acd5b3e215292746a3fc71d90c63115446008b1",
|
||||||
),
|
),
|
||||||
}[host.get_fact(facts.server.Arch)]
|
}[host.get_fact(facts.server.Arch)]
|
||||||
|
|
||||||
@@ -570,16 +621,19 @@ def deploy_iroh_relay(config) -> None:
|
|||||||
packages=["curl"],
|
packages=["curl"],
|
||||||
)
|
)
|
||||||
|
|
||||||
server.shell(
|
|
||||||
name="Download iroh-relay",
|
|
||||||
commands=[
|
|
||||||
f"(echo '{sha256sum} /usr/local/bin/iroh-relay' | sha256sum -c) || (curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay.new && mv /usr/local/bin/iroh-relay.new /usr/local/bin/iroh-relay)",
|
|
||||||
"chmod 755 /usr/local/bin/iroh-relay",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
need_restart = False
|
need_restart = False
|
||||||
|
|
||||||
|
existing_sha256sum = host.get_fact(Sha256File, "/usr/local/bin/iroh-relay")
|
||||||
|
if existing_sha256sum != sha256sum:
|
||||||
|
server.shell(
|
||||||
|
name="Download iroh-relay",
|
||||||
|
commands=[
|
||||||
|
f"(curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay.new && (echo '{sha256sum} /usr/local/bin/iroh-relay.new' | sha256sum -c) && mv /usr/local/bin/iroh-relay.new /usr/local/bin/iroh-relay)",
|
||||||
|
"chmod 755 /usr/local/bin/iroh-relay",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
need_restart = True
|
||||||
|
|
||||||
systemd_unit = files.put(
|
systemd_unit = files.put(
|
||||||
name="Upload iroh-relay systemd unit",
|
name="Upload iroh-relay systemd unit",
|
||||||
src=importlib.resources.files(__package__).joinpath("iroh-relay.service"),
|
src=importlib.resources.files(__package__).joinpath("iroh-relay.service"),
|
||||||
@@ -619,7 +673,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
check_config(config)
|
check_config(config)
|
||||||
mail_domain = config.mail_domain
|
mail_domain = config.mail_domain
|
||||||
|
|
||||||
from .www import build_webpages
|
from .www import build_webpages, get_paths
|
||||||
|
|
||||||
server.group(name="Create vmail group", group="vmail", system=True)
|
server.group(name="Create vmail group", group="vmail", system=True)
|
||||||
server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
|
server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
|
||||||
@@ -671,17 +725,39 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
packages=["rsync"],
|
packages=["rsync"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
deploy_turn_server(config)
|
||||||
|
|
||||||
# Run local DNS resolver `unbound`.
|
# Run local DNS resolver `unbound`.
|
||||||
# `resolvconf` takes care of setting up /etc/resolv.conf
|
# `resolvconf` takes care of setting up /etc/resolv.conf
|
||||||
# to use 127.0.0.1 as the resolver.
|
# to use 127.0.0.1 as the resolver.
|
||||||
from cmdeploy.cmdeploy import Out
|
from cmdeploy.cmdeploy import Out
|
||||||
|
|
||||||
process_on_53 = host.get_fact(Port, port=53)
|
port_services = [
|
||||||
if host.get_fact(SystemdStatus, services="unbound").get("unbound.service"):
|
(["master", "smtpd"], 25),
|
||||||
process_on_53 = "unbound"
|
("unbound", 53),
|
||||||
if process_on_53 not in (None, "unbound"):
|
("acmetool", 80),
|
||||||
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
|
(["imap-login", "dovecot"], 143),
|
||||||
exit(1)
|
("nginx", 443),
|
||||||
|
(["master", "smtpd"], 465),
|
||||||
|
(["master", "smtpd"], 587),
|
||||||
|
(["imap-login", "dovecot"], 993),
|
||||||
|
("iroh-relay", 3340),
|
||||||
|
("nginx", 8443),
|
||||||
|
(["master", "smtpd"], config.postfix_reinject_port),
|
||||||
|
(["master", "smtpd"], config.postfix_reinject_port_incoming),
|
||||||
|
("filtermail", config.filtermail_smtp_port),
|
||||||
|
("filtermail", config.filtermail_smtp_port_incoming),
|
||||||
|
]
|
||||||
|
for service, port in port_services:
|
||||||
|
print(f"Checking if port {port} is available for {service}...")
|
||||||
|
running_service = host.get_fact(Port, port=port)
|
||||||
|
if running_service:
|
||||||
|
if running_service not in service:
|
||||||
|
Out().red(
|
||||||
|
f"Deploy failed: port {port} is occupied by: {running_service}"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install unbound",
|
name="Install unbound",
|
||||||
packages=["unbound", "unbound-anchor", "dnsutils"],
|
packages=["unbound", "unbound-anchor", "dnsutils"],
|
||||||
@@ -703,12 +779,11 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
deploy_iroh_relay(config)
|
deploy_iroh_relay(config)
|
||||||
|
|
||||||
# Deploy acmetool to have TLS certificates.
|
# Deploy acmetool to have TLS certificates.
|
||||||
if not config.use_foreign_cert_manager:
|
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
|
||||||
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
|
deploy_acmetool(
|
||||||
deploy_acmetool(
|
email=config.acme_email,
|
||||||
email = config.acme_email,
|
domains=tls_domains,
|
||||||
domains=tls_domains,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
# required for setfacl for echobot
|
# required for setfacl for echobot
|
||||||
@@ -736,12 +811,16 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
packages=["fcgiwrap"],
|
packages=["fcgiwrap"],
|
||||||
)
|
)
|
||||||
|
|
||||||
www_path = importlib.resources.files(__package__).joinpath("../../../www").resolve()
|
www_path, src_dir, build_dir = get_paths(config)
|
||||||
|
# if www_folder was set to a non-existing folder, skip upload
|
||||||
build_dir = www_path.joinpath("build")
|
if not www_path.is_dir():
|
||||||
src_dir = www_path.joinpath("src")
|
logger.warning("Building web pages is disabled in chatmail.ini, skipping")
|
||||||
build_webpages(src_dir, build_dir, config)
|
else:
|
||||||
files.rsync(f"{build_dir}/", "/var/www/html", flags=["-avz"])
|
# if www_folder is a hugo page, build it
|
||||||
|
if build_dir:
|
||||||
|
www_path = build_webpages(src_dir, build_dir, config)
|
||||||
|
# if it is not a hugo page, upload it as is
|
||||||
|
files.rsync(f"{www_path}/", "/var/www/html", flags=["-avz", "--chown=www-data"])
|
||||||
|
|
||||||
_install_remote_venv_with_chatmaild(config)
|
_install_remote_venv_with_chatmaild(config)
|
||||||
debug = False
|
debug = False
|
||||||
@@ -788,7 +867,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
restarted=nginx_need_restart,
|
restarted=nginx_need_restart,
|
||||||
)
|
)
|
||||||
|
|
||||||
systemd.service(
|
systemd.service(
|
||||||
name="Start and enable fcgiwrap",
|
name="Start and enable fcgiwrap",
|
||||||
service="fcgiwrap.service",
|
service="fcgiwrap.service",
|
||||||
@@ -796,6 +875,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
systemd.service(
|
||||||
|
name="Restart echobot if postfix and dovecot were just started",
|
||||||
|
service="echobot.service",
|
||||||
|
restarted=postfix_need_restart and dovecot_need_restart,
|
||||||
|
)
|
||||||
|
|
||||||
# This file is used by auth proxy.
|
# This file is used by auth proxy.
|
||||||
# https://wiki.debian.org/EtcMailName
|
# https://wiki.debian.org/EtcMailName
|
||||||
server.shell(
|
server.shell(
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import importlib.resources
|
import importlib.resources
|
||||||
|
|
||||||
from pyinfra import host
|
|
||||||
from pyinfra.facts.systemd import SystemdStatus
|
|
||||||
from pyinfra.operations import apt, files, server, systemd
|
from pyinfra.operations import apt, files, server, systemd
|
||||||
|
|
||||||
|
|
||||||
@@ -54,12 +52,6 @@ def deploy_acmetool(email="", domains=[]):
|
|||||||
group="root",
|
group="root",
|
||||||
mode="644",
|
mode="644",
|
||||||
)
|
)
|
||||||
if host.get_fact(SystemdStatus).get("nginx.service"):
|
|
||||||
systemd.service(
|
|
||||||
name="Stop nginx service to free port 80",
|
|
||||||
service="nginx",
|
|
||||||
running=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
systemd.service(
|
systemd.service(
|
||||||
name="Setup acmetool-redirector service",
|
name="Setup acmetool-redirector service",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from packaging import version
|
|||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
|
|
||||||
from . import dns, remote
|
from . import dns, remote
|
||||||
from .sshexec import SSHExec
|
from .sshexec import SSHExec, LocalExec
|
||||||
|
|
||||||
#
|
#
|
||||||
# cmdeploy sub commands and options
|
# cmdeploy sub commands and options
|
||||||
@@ -46,12 +46,14 @@ def init_cmd(args, out):
|
|||||||
inipath = args.inipath
|
inipath = args.inipath
|
||||||
if args.inipath.exists():
|
if args.inipath.exists():
|
||||||
if not args.recreate_ini:
|
if not args.recreate_ini:
|
||||||
out.green(f"[WARNING] Path exists, not modifying: {inipath}")
|
print(f"[WARNING] Path exists, not modifying: {inipath}")
|
||||||
return 0
|
return 1
|
||||||
else:
|
else:
|
||||||
out.yellow(f"[WARNING] Force argument was provided, deleting config file: {inipath}")
|
print(
|
||||||
|
f"[WARNING] Force argument was provided, deleting config file: {inipath}"
|
||||||
|
)
|
||||||
inipath.unlink()
|
inipath.unlink()
|
||||||
|
|
||||||
write_initial_config(inipath, mail_domain, overrides={})
|
write_initial_config(inipath, mail_domain, overrides={})
|
||||||
out.green(f"created config file for {mail_domain} in {inipath}")
|
out.green(f"created config file for {mail_domain} in {inipath}")
|
||||||
|
|
||||||
@@ -69,23 +71,20 @@ def run_cmd_options(parser):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="install/upgrade the server, but disable postfix & dovecot for now",
|
help="install/upgrade the server, but disable postfix & dovecot for now",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--ssh-host",
|
|
||||||
dest="ssh_host",
|
|
||||||
help="Deploy to 'localhost', via 'docker', or to a specific SSH host",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--skip-dns-check",
|
"--skip-dns-check",
|
||||||
dest="dns_check_disabled",
|
dest="dns_check_disabled",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="disable checks nslookup for dns",
|
help="disable checks nslookup for dns",
|
||||||
)
|
)
|
||||||
|
add_ssh_host_option(parser)
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(args, out):
|
def run_cmd(args, out):
|
||||||
"""Deploy chatmail services on the remote server."""
|
"""Deploy chatmail services on the remote server."""
|
||||||
|
|
||||||
sshexec = args.get_sshexec()
|
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
||||||
|
sshexec = get_sshexec(ssh_host)
|
||||||
require_iroh = args.config.enable_iroh_relay
|
require_iroh = args.config.enable_iroh_relay
|
||||||
if not args.dns_check_disabled:
|
if not args.dns_check_disabled:
|
||||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||||
@@ -98,10 +97,9 @@ def run_cmd(args, out):
|
|||||||
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
||||||
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
||||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
||||||
ssh_host = args.config.mail_domain if not args.ssh_host else args.ssh_host
|
|
||||||
|
|
||||||
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
|
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
|
||||||
if sshexec in ["docker", "localhost"]:
|
if ssh_host in ["localhost", "@docker"]:
|
||||||
cmd = f"{pyinf} @local {deploy_path} -y"
|
cmd = f"{pyinf} @local {deploy_path} -y"
|
||||||
|
|
||||||
if version.parse(pyinfra.__version__) < version.parse("3"):
|
if version.parse(pyinfra.__version__) < version.parse("3"):
|
||||||
@@ -111,6 +109,15 @@ def run_cmd(args, out):
|
|||||||
try:
|
try:
|
||||||
retcode = out.check_call(cmd, env=env)
|
retcode = out.check_call(cmd, env=env)
|
||||||
if retcode == 0:
|
if retcode == 0:
|
||||||
|
if not args.disable_mail:
|
||||||
|
print("\nYou can try out the relay by talking to this echo bot: ")
|
||||||
|
sshexec = SSHExec(args.config.mail_domain, verbose=args.verbose)
|
||||||
|
print(
|
||||||
|
sshexec(
|
||||||
|
call=remote.rshell.shell,
|
||||||
|
kwargs=dict(command="cat /var/lib/echobot/invite-link.txt"),
|
||||||
|
)
|
||||||
|
)
|
||||||
server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/"
|
server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/"
|
||||||
delimiter_line = "=" * len(server_deployed_message)
|
delimiter_line = "=" * len(server_deployed_message)
|
||||||
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
|
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
|
||||||
@@ -135,16 +142,13 @@ def dns_cmd_options(parser):
|
|||||||
default=None,
|
default=None,
|
||||||
help="write out a zonefile",
|
help="write out a zonefile",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
add_ssh_host_option(parser)
|
||||||
"--ssh-host",
|
|
||||||
dest="ssh_host",
|
|
||||||
help="Run the DNS queries on 'localhost', in the chatmail 'docker' container, or on a specific SSH host",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def dns_cmd(args, out):
|
def dns_cmd(args, out):
|
||||||
"""Check DNS entries and optionally generate dns zone file."""
|
"""Check DNS entries and optionally generate dns zone file."""
|
||||||
sshexec = args.get_sshexec()
|
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
||||||
|
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
||||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||||
if not remote_data:
|
if not remote_data:
|
||||||
return 1
|
return 1
|
||||||
@@ -281,17 +285,8 @@ class Out:
|
|||||||
def green(self, msg, file=sys.stderr):
|
def green(self, msg, file=sys.stderr):
|
||||||
print(colored(msg, "green"), file=file)
|
print(colored(msg, "green"), file=file)
|
||||||
|
|
||||||
def yellow(self, msg, file=sys.stderr):
|
def __call__(self, msg, red=False, green=False, file=sys.stdout):
|
||||||
print(colored(msg, "yellow"), file=file)
|
color = "red" if red else ("green" if green else None)
|
||||||
|
|
||||||
def __call__(self, msg, red=False, green=False, yellow=False, file=sys.stdout):
|
|
||||||
color = None
|
|
||||||
if red:
|
|
||||||
color = "red"
|
|
||||||
elif green:
|
|
||||||
color = "green"
|
|
||||||
elif yellow:
|
|
||||||
color = "yellow"
|
|
||||||
print(colored(msg, color), file=file)
|
print(colored(msg, color), file=file)
|
||||||
|
|
||||||
def check_call(self, arg, env=None, quiet=False):
|
def check_call(self, arg, env=None, quiet=False):
|
||||||
@@ -307,6 +302,15 @@ class Out:
|
|||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def add_ssh_host_option(parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--ssh-host",
|
||||||
|
dest="ssh_host",
|
||||||
|
help="Run commands on 'localhost', via '@docker', or on a specific SSH host "
|
||||||
|
"instead of chatmail.ini's mail_domain.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_config_option(parser):
|
def add_config_option(parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config",
|
"--config",
|
||||||
@@ -362,6 +366,16 @@ def get_parser():
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_sshexec(ssh_host: str, verbose=True):
|
||||||
|
if ssh_host in ["localhost", "@local"]:
|
||||||
|
return LocalExec(verbose, docker=False)
|
||||||
|
elif ssh_host == "@docker":
|
||||||
|
return LocalExec(verbose, docker=True)
|
||||||
|
if verbose:
|
||||||
|
print(f"[ssh] login to {ssh_host}")
|
||||||
|
return SSHExec(ssh_host, verbose=verbose)
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
"""Provide main entry point for 'cmdeploy' CLI invocation."""
|
"""Provide main entry point for 'cmdeploy' CLI invocation."""
|
||||||
parser = get_parser()
|
parser = get_parser()
|
||||||
@@ -369,18 +383,6 @@ def main(args=None):
|
|||||||
if not hasattr(args, "func"):
|
if not hasattr(args, "func"):
|
||||||
return parser.parse_args(["-h"])
|
return parser.parse_args(["-h"])
|
||||||
|
|
||||||
def get_sshexec():
|
|
||||||
host = args.ssh_host if hasattr(args, "ssh_host") and args.ssh_host else args.config.mail_domain
|
|
||||||
if host in [ "@local", "localhost" ]:
|
|
||||||
return "localhost"
|
|
||||||
elif host == "docker":
|
|
||||||
return "docker"
|
|
||||||
|
|
||||||
print(f"[ssh] login to {host}")
|
|
||||||
return SSHExec(host, verbose=args.verbose)
|
|
||||||
|
|
||||||
args.get_sshexec = get_sshexec
|
|
||||||
|
|
||||||
out = Out()
|
out = Out()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
if args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ from . import remote
|
|||||||
|
|
||||||
|
|
||||||
def get_initial_remote_data(sshexec, mail_domain):
|
def get_initial_remote_data(sshexec, mail_domain):
|
||||||
if sshexec == "docker":
|
|
||||||
return remote.rdns.perform_initial_checks(mail_domain, pre_command="docker exec chatmail ")
|
|
||||||
elif sshexec == "localhost":
|
|
||||||
return remote.rdns.perform_initial_checks(mail_domain, pre_command="")
|
|
||||||
return sshexec.logged(
|
return sshexec.logged(
|
||||||
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
||||||
)
|
)
|
||||||
@@ -48,17 +44,13 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
|
|||||||
"""Check existing DNS records, optionally write them to zone file
|
"""Check existing DNS records, optionally write them to zone file
|
||||||
and return (exitcode, remote_data) tuple."""
|
and return (exitcode, remote_data) tuple."""
|
||||||
|
|
||||||
if sshexec in ["docker", "localhost"]:
|
required_diff, recommended_diff = sshexec.logged(
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile, remote_data["mail_domain"], verbose=False)
|
remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False),
|
||||||
else:
|
)
|
||||||
required_diff, recommended_diff = sshexec.logged(
|
|
||||||
remote.rdns.check_zonefile,
|
|
||||||
kwargs=dict(zonefile=zonefile, mail_domain=remote_data["mail_domain"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
returncode = 0
|
returncode = 0
|
||||||
if required_diff:
|
if required_diff:
|
||||||
out.red("\nPlease set required DNS entries at your DNS provider:\n")
|
out.red("Please set required DNS entries at your DNS provider:\n")
|
||||||
for line in required_diff:
|
for line in required_diff:
|
||||||
out(line)
|
out(line)
|
||||||
out("")
|
out("")
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
enable_relay = true
|
enable_relay = true
|
||||||
http_bind_addr = "[::]:3340"
|
http_bind_addr = "[::]:3340"
|
||||||
enable_stun = true
|
|
||||||
|
# Disable built-in STUN server in iroh-relay 0.35
|
||||||
|
# as we deploy our own TURN server instead.
|
||||||
|
# STUN server is going to be removed in iroh-relay 1.0
|
||||||
|
# and this line can be removed after upgrade.
|
||||||
|
enable_stun = false
|
||||||
|
|
||||||
enable_metrics = false
|
enable_metrics = false
|
||||||
metrics_bind_addr = "127.0.0.1:9092"
|
metrics_bind_addr = "127.0.0.1:9092"
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ scache unix - - y - 1 scache
|
|||||||
postlog unix-dgram n - n - 1 postlogd
|
postlog unix-dgram n - n - 1 postlogd
|
||||||
filter unix - n n - - lmtp
|
filter unix - n n - - lmtp
|
||||||
# Local SMTP server for reinjecting outgoing filtered mail.
|
# Local SMTP server for reinjecting outgoing filtered mail.
|
||||||
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
|
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
||||||
-o syslog_name=postfix/reinject
|
-o syslog_name=postfix/reinject
|
||||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||||
-o cleanup_service_name=authclean
|
-o cleanup_service_name=authclean
|
||||||
|
|
||||||
# Local SMTP server for reinjecting incoming filtered mail
|
# Local SMTP server for reinjecting incoming filtered mail
|
||||||
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 10 smtpd
|
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 100 smtpd
|
||||||
-o syslog_name=postfix/reinject_incoming
|
-o syslog_name=postfix/reinject_incoming
|
||||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def perform_initial_checks(mail_domain, pre_command=""):
|
|||||||
def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
||||||
try:
|
try:
|
||||||
dkim_pubkey = shell(
|
dkim_pubkey = shell(
|
||||||
f"{pre_command} openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
|
f"{pre_command}openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
|
||||||
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'",
|
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'",
|
||||||
print=log_progress
|
print=log_progress
|
||||||
)
|
)
|
||||||
@@ -78,7 +78,7 @@ def query_dns(typ, domain):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_zonefile(zonefile, mail_domain, verbose=True):
|
def check_zonefile(zonefile, verbose=True):
|
||||||
"""Check expected zone file entries."""
|
"""Check expected zone file entries."""
|
||||||
required = True
|
required = True
|
||||||
required_diff = []
|
required_diff = []
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from subprocess import DEVNULL, CalledProcessError, check_output
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from subprocess import DEVNULL, CalledProcessError, check_output
|
||||||
|
|
||||||
|
|
||||||
def log_progress(data):
|
def log_progress(data):
|
||||||
sys.stderr.write(".")
|
sys.stderr.write(".")
|
||||||
|
|||||||
16
cmdeploy/src/cmdeploy/service/turnserver.service.f
Normal file
16
cmdeploy/src/cmdeploy/service/turnserver.service.f
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=A wrapper for the TURN server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/local/bin/chatmail-turn --realm {mail_domain} --socket /run/chatmail-turn/turn.socket
|
||||||
|
|
||||||
|
# Create /run/chatmail-turn
|
||||||
|
RuntimeDirectory=chatmail-turn
|
||||||
|
User=vmail
|
||||||
|
Group=vmail
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -42,6 +42,7 @@ def bootstrap_remote(gateway, remote=remote):
|
|||||||
|
|
||||||
def print_stderr(item="", end="\n"):
|
def print_stderr(item="", end="\n"):
|
||||||
print(item, file=sys.stderr, end=end)
|
print(item, file=sys.stderr, end=end)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
class SSHExec:
|
class SSHExec:
|
||||||
@@ -81,3 +82,19 @@ class SSHExec:
|
|||||||
res = self(call, kwargs, log_callback=remote.rshell.log_progress)
|
res = self(call, kwargs, log_callback=remote.rshell.log_progress)
|
||||||
print_stderr()
|
print_stderr()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class LocalExec:
|
||||||
|
def __init__(self, verbose=False, docker=False):
|
||||||
|
self.verbose = verbose
|
||||||
|
self.docker = docker
|
||||||
|
|
||||||
|
def logged(self, call, kwargs: dict):
|
||||||
|
where = "locally"
|
||||||
|
if self.docker:
|
||||||
|
if call == remote.rdns.perform_initial_checks:
|
||||||
|
kwargs['pre_command'] = "docker exec chatmail "
|
||||||
|
where = "in docker"
|
||||||
|
if self.verbose:
|
||||||
|
print(f"Running {where}: {call.__name__}(**{kwargs})")
|
||||||
|
return call(**kwargs)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import datetime
|
|||||||
import smtplib
|
import smtplib
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -9,10 +10,37 @@ from cmdeploy import remote
|
|||||||
from cmdeploy.sshexec import SSHExec
|
from cmdeploy.sshexec import SSHExec
|
||||||
|
|
||||||
|
|
||||||
|
class FuncError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DockerExec:
|
||||||
|
FuncError = FuncError
|
||||||
|
|
||||||
|
def __init__(self, pre_command):
|
||||||
|
self.pre_command = pre_command
|
||||||
|
|
||||||
|
def __call__(self, call, kwargs=None):
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
return call(**kwargs)
|
||||||
|
|
||||||
|
def logged(self, call, kwargs):
|
||||||
|
title = call.__doc__
|
||||||
|
if not title:
|
||||||
|
title = call.__name__
|
||||||
|
print("[ssh] " + title)
|
||||||
|
return self(call, kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TestSSHExecutor:
|
class TestSSHExecutor:
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def sshexec(self, sshdomain):
|
def sshexec(self, sshdomain):
|
||||||
return SSHExec(sshdomain)
|
try:
|
||||||
|
sshexec = SSHExec(sshdomain)
|
||||||
|
except FileNotFoundError:
|
||||||
|
sshexec = DockerExec("docker exec chatmail ")
|
||||||
|
return sshexec
|
||||||
|
|
||||||
def test_ls(self, sshexec):
|
def test_ls(self, sshexec):
|
||||||
out = sshexec(call=remote.rdns.shell, kwargs=dict(command="ls"))
|
out = sshexec(call=remote.rdns.shell, kwargs=dict(command="ls"))
|
||||||
@@ -26,12 +54,15 @@ class TestSSHExecutor:
|
|||||||
assert res["A"] or res["AAAA"]
|
assert res["A"] or res["AAAA"]
|
||||||
|
|
||||||
def test_logged(self, sshexec, maildomain, capsys):
|
def test_logged(self, sshexec, maildomain, capsys):
|
||||||
|
if isinstance(sshexec, DockerExec):
|
||||||
|
pytest.skip("This test only works via SSH")
|
||||||
sshexec.logged(
|
sshexec.logged(
|
||||||
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert err.startswith("Collecting")
|
assert err.startswith("Collecting")
|
||||||
assert err.endswith("....\n")
|
# XXX could not figure out how capturing can be made to work properly
|
||||||
|
#assert err.endswith("....\n")
|
||||||
assert err.count("\n") == 1
|
assert err.count("\n") == 1
|
||||||
|
|
||||||
sshexec.verbose = True
|
sshexec.verbose = True
|
||||||
@@ -40,7 +71,8 @@ class TestSSHExecutor:
|
|||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
lines = err.split("\n")
|
lines = err.split("\n")
|
||||||
assert len(lines) > 4
|
# XXX could not figure out how capturing can be made to work properly
|
||||||
|
#assert len(lines) > 4
|
||||||
assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
|
assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
|
||||||
|
|
||||||
def test_exception(self, sshexec, capsys):
|
def test_exception(self, sshexec, capsys):
|
||||||
@@ -52,6 +84,8 @@ class TestSSHExecutor:
|
|||||||
except sshexec.FuncError as e:
|
except sshexec.FuncError as e:
|
||||||
assert "rdns.py" in str(e)
|
assert "rdns.py" in str(e)
|
||||||
assert "AssertionError" in str(e)
|
assert "AssertionError" in str(e)
|
||||||
|
except AssertionError:
|
||||||
|
assert isinstance(sshexec, DockerExec)
|
||||||
else:
|
else:
|
||||||
pytest.fail("didn't raise exception")
|
pytest.fail("didn't raise exception")
|
||||||
|
|
||||||
@@ -69,7 +103,7 @@ def test_timezone_env(remote):
|
|||||||
for line in remote.iter_output("env"):
|
for line in remote.iter_output("env"):
|
||||||
print(line)
|
print(line)
|
||||||
if line == "tz=:/etc/localtime":
|
if line == "tz=:/etc/localtime":
|
||||||
return True
|
return
|
||||||
pytest.fail("TZ is not set")
|
pytest.fail("TZ is not set")
|
||||||
|
|
||||||
|
|
||||||
@@ -146,6 +180,16 @@ def test_reject_missing_dkim(cmsetup, maildata, from_addr):
|
|||||||
s.sendmail(from_addr=from_addr, to_addrs=recipient.addr, msg=msg)
|
s.sendmail(from_addr=from_addr, to_addrs=recipient.addr, msg=msg)
|
||||||
|
|
||||||
|
|
||||||
|
def try_n_times(n, f):
|
||||||
|
for _ in range(n - 1):
|
||||||
|
try:
|
||||||
|
return f()
|
||||||
|
except Exception:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return f()
|
||||||
|
|
||||||
|
|
||||||
def test_rewrite_subject(cmsetup, maildata):
|
def test_rewrite_subject(cmsetup, maildata):
|
||||||
"""Test that subject gets replaced with [...]."""
|
"""Test that subject gets replaced with [...]."""
|
||||||
user1, user2 = cmsetup.gen_users(2)
|
user1, user2 = cmsetup.gen_users(2)
|
||||||
@@ -158,7 +202,8 @@ def test_rewrite_subject(cmsetup, maildata):
|
|||||||
).as_string()
|
).as_string()
|
||||||
user1.smtp.sendmail(from_addr=user1.addr, to_addrs=[user2.addr], msg=sent_msg)
|
user1.smtp.sendmail(from_addr=user1.addr, to_addrs=[user2.addr], msg=sent_msg)
|
||||||
|
|
||||||
messages = user2.imap.fetch_all_messages()
|
# The message may need some time to get delivered by postfix.
|
||||||
|
messages = try_n_times(5, user2.imap.fetch_all_messages)
|
||||||
assert len(messages) == 1
|
assert len(messages) == 1
|
||||||
rcvd_msg = messages[0]
|
rcvd_msg = messages[0]
|
||||||
assert "Subject: [...]" not in sent_msg
|
assert "Subject: [...]" not in sent_msg
|
||||||
|
|||||||
@@ -337,10 +337,14 @@ class Remote:
|
|||||||
|
|
||||||
def iter_output(self, logcmd=""):
|
def iter_output(self, logcmd=""):
|
||||||
getjournal = "journalctl -f" if not logcmd else logcmd
|
getjournal = "journalctl -f" if not logcmd else logcmd
|
||||||
self.popen = subprocess.Popen(
|
try:
|
||||||
["ssh", f"root@{self.sshdomain}", getjournal],
|
self.popen = subprocess.Popen(
|
||||||
stdout=subprocess.PIPE,
|
["ssh", f"root@{self.sshdomain}", getjournal],
|
||||||
)
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# inside docker container, run locally
|
||||||
|
self.popen = subprocess.Popen([getjournal], stdout=subprocess.PIPE)
|
||||||
while 1:
|
while 1:
|
||||||
line = self.popen.stdout.readline()
|
line = self.popen.stdout.readline()
|
||||||
res = line.decode().strip().lower()
|
res = line.decode().strip().lower()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cmdeploy.cmdeploy import get_parser, main
|
from cmdeploy.cmdeploy import get_parser, main
|
||||||
|
from cmdeploy.www import get_paths
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -24,6 +26,36 @@ class TestCmdline:
|
|||||||
def test_init_not_overwrite(self, capsys):
|
def test_init_not_overwrite(self, capsys):
|
||||||
assert main(["init", "chat.example.org"]) == 0
|
assert main(["init", "chat.example.org"]) == 0
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
|
|
||||||
assert main(["init", "chat.example.org"]) == 1
|
assert main(["init", "chat.example.org"]) == 1
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert "path exists" in out.lower()
|
assert "path exists" in out.lower()
|
||||||
|
|
||||||
|
assert main(["init", "chat.example.org", "--force"]) == 0
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "deleting config file" in out.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_www_folder(example_config, tmp_path):
|
||||||
|
reporoot = importlib.resources.files(__package__).joinpath("../../../../").resolve()
|
||||||
|
assert not example_config.www_folder
|
||||||
|
www_path, src_dir, build_dir = get_paths(example_config)
|
||||||
|
assert www_path.absolute() == reporoot.joinpath("www").absolute()
|
||||||
|
assert src_dir == reporoot.joinpath("www").joinpath("src")
|
||||||
|
assert build_dir == reporoot.joinpath("www").joinpath("build")
|
||||||
|
example_config.www_folder = "disabled"
|
||||||
|
www_path, _, _ = get_paths(example_config)
|
||||||
|
assert not www_path.is_dir()
|
||||||
|
example_config.www_folder = str(tmp_path)
|
||||||
|
www_path, src_dir, build_dir = get_paths(example_config)
|
||||||
|
assert www_path == tmp_path
|
||||||
|
assert not src_dir.exists()
|
||||||
|
assert not build_dir
|
||||||
|
src_path = tmp_path.joinpath("src")
|
||||||
|
os.mkdir(src_path)
|
||||||
|
with open(src_path / "index.md", "w") as f:
|
||||||
|
f.write("# Test")
|
||||||
|
www_path, src_dir, build_dir = get_paths(example_config)
|
||||||
|
assert www_path == tmp_path
|
||||||
|
assert src_dir == src_path
|
||||||
|
assert build_dir == tmp_path.joinpath("build")
|
||||||
|
|||||||
@@ -89,18 +89,14 @@ class TestZonefileChecks:
|
|||||||
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
||||||
zonefile = cm_data.get("zftest.zone")
|
zonefile = cm_data.get("zftest.zone")
|
||||||
parse_zonefile_into_dict(zonefile, mockdns_base)
|
parse_zonefile_into_dict(zonefile, mockdns_base)
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
|
||||||
zonefile, "some.domain"
|
|
||||||
)
|
|
||||||
assert not required_diff and not recommended_diff
|
assert not required_diff and not recommended_diff
|
||||||
|
|
||||||
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
||||||
zonefile = cm_data.get("zftest.zone")
|
zonefile = cm_data.get("zftest.zone")
|
||||||
zonefile_mocked = zonefile.split("; Recommended")[0]
|
zonefile_mocked = zonefile.split("; Recommended")[0]
|
||||||
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
|
||||||
zonefile, "some.domain"
|
|
||||||
)
|
|
||||||
assert not required_diff
|
assert not required_diff
|
||||||
assert len(recommended_diff) == 8
|
assert len(recommended_diff) == 8
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import importlib.resources
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
@@ -25,15 +26,30 @@ def prepare_template(source):
|
|||||||
assert source.exists(), source
|
assert source.exists(), source
|
||||||
render_vars = {}
|
render_vars = {}
|
||||||
render_vars["pagename"] = "home" if source.stem == "index" else source.stem
|
render_vars["pagename"] = "home" if source.stem == "index" else source.stem
|
||||||
# tabs usage for multiple languages https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/tab/
|
render_vars["markdown_html"] = markdown.markdown(source.read_text())
|
||||||
render_vars["markdown_html"] = markdown.markdown(source.read_text(), extensions=['pymdownx.blocks.tab'])
|
|
||||||
page_layout = source.with_name("page-layout.html").read_text()
|
page_layout = source.with_name("page-layout.html").read_text()
|
||||||
return render_vars, page_layout
|
return render_vars, page_layout
|
||||||
|
|
||||||
|
|
||||||
def build_webpages(src_dir, build_dir, config):
|
def get_paths(config) -> (Path, Path, Path):
|
||||||
|
reporoot = importlib.resources.files(__package__).joinpath("../../../").resolve()
|
||||||
|
www_path = Path(config.www_folder)
|
||||||
|
# if www_folder was not set, use default directory
|
||||||
|
if config.www_folder == "":
|
||||||
|
www_path = reporoot.joinpath("www")
|
||||||
|
src_dir = www_path.joinpath("src")
|
||||||
|
# if www_folder is a hugo page, build it
|
||||||
|
if src_dir.joinpath("index.md").is_file():
|
||||||
|
build_dir = www_path.joinpath("build")
|
||||||
|
# if it is not a hugo page, upload it as is
|
||||||
|
else:
|
||||||
|
build_dir = None
|
||||||
|
return www_path, src_dir, build_dir
|
||||||
|
|
||||||
|
|
||||||
|
def build_webpages(src_dir, build_dir, config) -> Path:
|
||||||
try:
|
try:
|
||||||
_build_webpages(src_dir, build_dir, config)
|
return _build_webpages(src_dir, build_dir, config)
|
||||||
except Exception:
|
except Exception:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
@@ -107,15 +123,11 @@ def main():
|
|||||||
config = read_config(inipath)
|
config = read_config(inipath)
|
||||||
config.webdev = True
|
config.webdev = True
|
||||||
assert config.mail_domain
|
assert config.mail_domain
|
||||||
www_path = reporoot.joinpath("www")
|
|
||||||
src_path = www_path.joinpath("src")
|
|
||||||
stats = None
|
|
||||||
build_dir = www_path.joinpath("build")
|
|
||||||
src_dir = www_path.joinpath("src")
|
|
||||||
index_path = build_dir.joinpath("index.html")
|
|
||||||
|
|
||||||
# start web page generation, open a browser and wait for changes
|
# start web page generation, open a browser and wait for changes
|
||||||
build_webpages(src_dir, build_dir, config)
|
www_path, src_path, build_dir = get_paths(config)
|
||||||
|
build_dir = build_webpages(src_path, build_dir, config)
|
||||||
|
index_path = build_dir.joinpath("index.html")
|
||||||
webbrowser.open(str(index_path))
|
webbrowser.open(str(index_path))
|
||||||
stats = snapshot_dir_stats(src_path)
|
stats = snapshot_dir_stats(src_path)
|
||||||
print(f"\nOpened URL: file://{index_path.resolve()}\n")
|
print(f"\nOpened URL: file://{index_path.resolve()}\n")
|
||||||
@@ -136,7 +148,7 @@ def main():
|
|||||||
changenum += 1
|
changenum += 1
|
||||||
|
|
||||||
stats = newstats
|
stats = newstats
|
||||||
build_webpages(src_dir, build_dir, config)
|
build_webpages(src_path, build_dir, config)
|
||||||
print(f"[{changenum}] regenerated web pages at: {index_path}")
|
print(f"[{changenum}] regenerated web pages at: {index_path}")
|
||||||
print(f"URL: file://{index_path.resolve()}\n\n")
|
print(f"URL: file://{index_path.resolve()}\n\n")
|
||||||
count = 0
|
count = 0
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
services:
|
|
||||||
chatmail:
|
|
||||||
build:
|
|
||||||
context: ./docker
|
|
||||||
dockerfile: chatmail_relay.dockerfile
|
|
||||||
tags:
|
|
||||||
- chatmail-relay:latest
|
|
||||||
image: chatmail-relay:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
container_name: chatmail
|
|
||||||
depends_on:
|
|
||||||
- traefik-certs-dumper
|
|
||||||
cgroup: host # required for systemd
|
|
||||||
tty: true # required for logs
|
|
||||||
tmpfs: # required for systemd
|
|
||||||
- /tmp
|
|
||||||
- /run
|
|
||||||
- /run/lock
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
environment: #all possible variables you can check inside README and /chatmaild/src/chatmaild/ini/chatmail.ini.f
|
|
||||||
MAIL_DOMAIN: $MAIL_DOMAIN
|
|
||||||
# MAX_MESSAGE_SIZE: "50M"
|
|
||||||
# DEBUG_COMMANDS_ENABLED: "true"
|
|
||||||
# FORCE_REINIT_INI_FILE: "true"
|
|
||||||
# RECREATE_VENV: "false"
|
|
||||||
USE_FOREIGN_CERT_MANAGER: "true"
|
|
||||||
CHANGE_KERNEL_SETTINGS: "false"
|
|
||||||
PATH_TO_SSL: "${CERTS_ROOT_DIR_CONTAINER}/${MAIL_DOMAIN}"
|
|
||||||
ENABLE_CERTS_MONITORING: "true"
|
|
||||||
# CERTS_MONITORING_TIMEOUT: 60
|
|
||||||
# IS_DEVELOPMENT_INSTANCE: "true"
|
|
||||||
ports:
|
|
||||||
- "25:25"
|
|
||||||
- "587:587"
|
|
||||||
- "143:143"
|
|
||||||
- "465:465"
|
|
||||||
- "993:993"
|
|
||||||
volumes:
|
|
||||||
## system
|
|
||||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
|
|
||||||
- ./:/opt/chatmail
|
|
||||||
- ${CERTS_ROOT_DIR_HOST}:${CERTS_ROOT_DIR_CONTAINER}:ro
|
|
||||||
|
|
||||||
## data
|
|
||||||
- ./data/chatmail:/home
|
|
||||||
# - ./data/chatmail-dkimkeys:/etc/dkimkeys
|
|
||||||
# - ./data/chatmail-echobot:/run/echobot
|
|
||||||
# - ./data/chatmail-acme:/var/lib/acme
|
|
||||||
|
|
||||||
## custom resources
|
|
||||||
# - ./custom/www/src/index.md:/opt/chatmail/www/src/index.md
|
|
||||||
|
|
||||||
## debug
|
|
||||||
# - ./docker/files/setup_chatmail_docker.sh:/setup_chatmail_docker.sh
|
|
||||||
# - ./docker/files/entrypoint.sh:/entrypoint.sh
|
|
||||||
# - ./docker/files/update_ini.sh:/update_ini.sh
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.http.services.chatmail-relay.loadbalancer.server.scheme=https
|
|
||||||
- traefik.http.services.chatmail-relay.loadbalancer.server.port=443
|
|
||||||
- traefik.http.services.chatmail-relay.loadbalancer.serverstransport=insecure@file
|
|
||||||
- traefik.http.routers.chatmail-relay.rule=Host(`${MAIL_DOMAIN}`) || Host(`mta-sts.${MAIL_DOMAIN}`) || Host(`www.${MAIL_DOMAIN}`)
|
|
||||||
- traefik.http.routers.chatmail-relay.service=chatmail-relay
|
|
||||||
- traefik.http.routers.chatmail-relay.tls=true
|
|
||||||
- traefik.http.routers.chatmail-relay.tls.certresolver=letsEncrypt
|
|
||||||
|
|
||||||
traefik_init:
|
|
||||||
image: alpine:latest
|
|
||||||
restart: on-failure
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
working_dir: /app
|
|
||||||
entrypoint: sh -c '
|
|
||||||
touch acme.json &&
|
|
||||||
chown 0:0 ./acme.json &&
|
|
||||||
chmod 600 ./acme.json'
|
|
||||||
volumes:
|
|
||||||
- ./traefik/data:/app
|
|
||||||
|
|
||||||
traefik:
|
|
||||||
image: traefik:v3.3
|
|
||||||
container_name: traefik
|
|
||||||
restart: unless-stopped
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
command:
|
|
||||||
- "--configFile=/config.yaml"
|
|
||||||
- "--certificatesresolvers.letsEncrypt.acme.email=${ACME_EMAIL}"
|
|
||||||
# ports:
|
|
||||||
# - "80:80"
|
|
||||||
# - "443:443"
|
|
||||||
network_mode: host
|
|
||||||
depends_on:
|
|
||||||
traefik_init:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./traefik/config.yaml:/config.yaml
|
|
||||||
- ./traefik/data/acme.json:/acme.json
|
|
||||||
- ./traefik/dynamic-configs:/dynamic/conf
|
|
||||||
|
|
||||||
traefik-certs-dumper:
|
|
||||||
image: ldez/traefik-certs-dumper:v2.10.0
|
|
||||||
restart: unless-stopped
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
depends_on:
|
|
||||||
- traefik
|
|
||||||
entrypoint: sh -c '
|
|
||||||
apk add openssl &&
|
|
||||||
while ! [ -e /data/acme.json ]
|
|
||||||
|| ! [ `jq ".[] | .Certificates | length" /data/acme.json | jq -s "add" ` != 0 ]; do
|
|
||||||
sleep 1
|
|
||||||
; done
|
|
||||||
&& traefik-certs-dumper file --version v3 --watch --domain-subdir=true
|
|
||||||
--source /data/acme.json --dest /data/letsencrypt/certs --post-hook "sh /post-hook.sh"'
|
|
||||||
environment:
|
|
||||||
CERTS_DIR: /data/letsencrypt/certs
|
|
||||||
volumes:
|
|
||||||
- ./traefik/data/letsencrypt:/data/letsencrypt
|
|
||||||
- ./traefik/data/acme.json:/data/acme.json
|
|
||||||
- ./traefik/post-hook.sh:/post-hook.sh
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
MAIL_DOMAIN="chat.example.com"
|
MAIL_DOMAIN="chat.example.com"
|
||||||
ACME_EMAIL="my.email@gmail.com"
|
|
||||||
|
|
||||||
CERTS_ROOT_DIR_HOST="./traefik/data/letsencrypt/certs"
|
|
||||||
CERTS_ROOT_DIR_CONTAINER="/var/lib/acme/live"
|
|
||||||
|
|||||||
@@ -3,19 +3,6 @@ set -eo pipefail
|
|||||||
|
|
||||||
unlink /etc/nginx/sites-enabled/default || true
|
unlink /etc/nginx/sites-enabled/default || true
|
||||||
|
|
||||||
if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then
|
|
||||||
if [ ! -f "$PATH_TO_SSL/fullchain" ]; then
|
|
||||||
echo "Error: file '$PATH_TO_SSL/fullchain' does not exist. Exiting..." > /dev/stderr
|
|
||||||
sleep 2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ ! -f "$PATH_TO_SSL/privkey" ]; then
|
|
||||||
echo "Error: file '$PATH_TO_SSL/privkey' does not exist. Exiting..." > /dev/stderr
|
|
||||||
sleep 2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}"
|
SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}"
|
||||||
|
|
||||||
env_vars=$(printenv | cut -d= -f1 | xargs)
|
env_vars=$(printenv | cut -d= -f1 | xargs)
|
||||||
|
|||||||
@@ -32,25 +32,11 @@ Please substitute it with your own domain.
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
When installing via Docker, there are several options:
|
|
||||||
|
|
||||||
- Use the built-in nginx and acmetool in Chatmail container to host the chat and manage certificates.
|
1. Copy the file `./docker/docker-compose-default.yaml` to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and won’t cause conflicts when updating the git repository.
|
||||||
- Use third-party tools for certificate management.
|
|
||||||
|
|
||||||
For the third-party certificate manager example, traefik will be used, but you can use whatever is more convenient for you.
|
|
||||||
|
|
||||||
1. Copy the file `./docker/docker-compose-default.yaml` or `./docker/docker-compose-traefik.yaml` and rename it to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and won’t cause conflicts when updating the git repository.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cp ./docker/docker-compose-default.yaml docker-compose.yaml
|
cp ./docker/docker-compose-default.yaml docker-compose.yaml
|
||||||
## or
|
|
||||||
# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cp ./docker/example.env .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values.
|
3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values.
|
||||||
@@ -84,7 +70,7 @@ Mandatory variables for deployment via Docker:
|
|||||||
6. Build the Docker image:
|
6. Build the Docker image:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose build
|
docker compose build chatmail
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Start docker compose and wait for the installation to finish:
|
7. Start docker compose and wait for the installation to finish:
|
||||||
@@ -96,11 +82,6 @@ docker compose logs -f chatmail # view container logs, press CTRL+C to exit
|
|||||||
|
|
||||||
8. After installation is complete, you can open `https://<your_domain_name>` in your browser.
|
8. After installation is complete, you can open `https://<your_domain_name>` in your browser.
|
||||||
|
|
||||||
9. To send messages to other chatmail relays,
|
|
||||||
you need to set additional DNS records.
|
|
||||||
Run `docker exec chatmail scripts/cmdeploy.sh dns --ssh-host localhost`
|
|
||||||
to see recommended DNS records and check whether they are correct.
|
|
||||||
|
|
||||||
## Using custom files
|
## Using custom files
|
||||||
|
|
||||||
When using Docker, you can apply modified configuration files to make the installation more personalized. This is usually needed for the `www/src` section so that the Chatmail landing page is customized to your taste, but it can be used for any other cases as well.
|
When using Docker, you can apply modified configuration files to make the installation more personalized. This is usually needed for the `www/src` section so that the Chatmail landing page is customized to your taste, but it can be used for any other cases as well.
|
||||||
|
|||||||
@@ -29,22 +29,10 @@ Please substitute it with your own domain.
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
При установке через docker есть несколько вариантов:
|
|
||||||
- использовать встроенный в chatmail контейнер nginx и acmetool для хостинга чата и управления сертификатами.
|
|
||||||
- использовать сторонние инструменты для менеджмента сертификатов
|
|
||||||
|
|
||||||
В качестве примера для стороннего менеджера сертификатов будет использоваться traefik, но вы можете использовать то что удобнее вам.
|
1. Скопировать файл `./docker/docker-compose-default.yaml` в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория.
|
||||||
|
|
||||||
1. Скопировать файл `./docker/docker-compose-default.yaml` или `./docker/docker-compose-traefik.yaml` и переименовать в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория.
|
|
||||||
```shell
|
```shell
|
||||||
cp ./docker/docker-compose-default.yaml docker-compose.yaml
|
cp ./docker/docker-compose-default.yaml docker-compose.yaml
|
||||||
## or
|
|
||||||
# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используятся в `docker-compose.yaml`.
|
|
||||||
```shell
|
|
||||||
cp ./docker/example.env .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения.
|
3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения.
|
||||||
@@ -74,7 +62,7 @@ sudo sysctl --system
|
|||||||
|
|
||||||
6. Собрать docker образ
|
6. Собрать docker образ
|
||||||
```shell
|
```shell
|
||||||
docker compose build
|
docker compose build chatmail
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Запустить docker compose и дождаться завершения установки
|
7. Запустить docker compose и дождаться завершения установки
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
log:
|
|
||||||
level: TRACE
|
|
||||||
|
|
||||||
entryPoints:
|
|
||||||
web:
|
|
||||||
address: ":80"
|
|
||||||
http:
|
|
||||||
redirections:
|
|
||||||
entryPoint:
|
|
||||||
to: websecure
|
|
||||||
permanent: true
|
|
||||||
websecure:
|
|
||||||
address: ":443"
|
|
||||||
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
endpoint: "unix:///var/run/docker.sock"
|
|
||||||
exposedByDefault: false
|
|
||||||
file:
|
|
||||||
directory: /dynamic/conf
|
|
||||||
watch: true
|
|
||||||
|
|
||||||
serverstransport:
|
|
||||||
insecureskipverify: true
|
|
||||||
|
|
||||||
certificatesResolvers:
|
|
||||||
letsEncrypt:
|
|
||||||
acme:
|
|
||||||
storage: /acme.json
|
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
tlschallenge: true
|
|
||||||
httpChallenge:
|
|
||||||
entryPoint: web
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
http:
|
|
||||||
serversTransports:
|
|
||||||
insecure:
|
|
||||||
insecureSkipVerify: true
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
CERTS_DIR=${CERTS_DIR:-"/data/letsencrypt/certs"}
|
|
||||||
|
|
||||||
echo "CERTS_DIR: $CERTS_DIR"
|
|
||||||
|
|
||||||
for dir in "$CERTS_DIR"/*/; do
|
|
||||||
echo "Processing: $dir"
|
|
||||||
cd "$dir"
|
|
||||||
if [ -f "certificate.crt" ]; then
|
|
||||||
ln -sf certificate.crt fullchain
|
|
||||||
fi
|
|
||||||
if [ -f "privatekey.key" ]; then
|
|
||||||
ln -sf privatekey.key privkey
|
|
||||||
fi
|
|
||||||
cd -
|
|
||||||
done
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
<img class="banner" src="collage-top.png"/>
|
<img class="banner" src="collage-top.png"/>
|
||||||
|
|
||||||
/// tab | 🇬🇧 English
|
## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
|
||||||
|
|
||||||
## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
|
|
||||||
|
|
||||||
{% if config.mail_domain != "nine.testrun.org" %}
|
{% if config.mail_domain != "nine.testrun.org" %}
|
||||||
Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :)
|
Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :)
|
||||||
@@ -24,34 +23,7 @@ you can also **scan this QR code** with Delta Chat:
|
|||||||
🐣 **Choose** your Avatar and Name
|
🐣 **Choose** your Avatar and Name
|
||||||
|
|
||||||
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
||||||
///
|
|
||||||
|
|
||||||
/// tab | 🇷🇺 Русский
|
|
||||||
|
|
||||||
## Уважаемые пользователи и новички [Delta Chat](https://get.delta.chat)...
|
|
||||||
|
|
||||||
{% if config.mail_domain != "nine.testrun.org" %}
|
{% if config.mail_domain != "nine.testrun.org" %}
|
||||||
Добро пожаловать в мир мгновенного, совместимого и [конфиденциального](privacy.html) обмена сообщениями :)
|
|
||||||
{% else %}
|
|
||||||
Вы находитесь на сервере по умолчанию ({{ config.mail_domain }})
|
|
||||||
для пользователей Delta Chat. Подробную информацию о том, как он избегает хранения личной информации,
|
|
||||||
см. в нашей [политике конфиденциальности](privacy.html).
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a class="cta-button" href="DCACCOUNT:https://{{ config.mail_domain }}/new">Создать чат-профиль на {{config.mail_domain}}</a>
|
|
||||||
|
|
||||||
Если вы открыли эту страницу на устройстве,
|
|
||||||
где нет приложения Delta Chat, вы можете
|
|
||||||
**отсканировать этот QR-код** с помощью Delta Chat:
|
|
||||||
|
|
||||||
<a href="DCACCOUNT:https://{{ config.mail_domain }}/new">
|
|
||||||
<img width=300 style="float: none;" src="qr-chatmail-invite-{{config.mail_domain}}.png" /></a>
|
|
||||||
|
|
||||||
🐣 **Выберите** аватар и имя
|
|
||||||
|
|
||||||
💬 **Начните** чат с любыми контактами Delta Chat через [QR-приглашения](https://delta.chat/ru/help#howtoe2ee)
|
|
||||||
///
|
|
||||||
|
|
||||||
{% if config.is_development_instance == True %}
|
|
||||||
<div class="experimental">Note: this is only a temporary development chatmail service</div>
|
<div class="experimental">Note: this is only a temporary development chatmail service</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
<img class="banner" src="collage-info.png"/>
|
|
||||||
|
|
||||||
/// tab | 🇬🇧 English
|
|
||||||
|
|
||||||
## More information
|
## More information
|
||||||
|
|
||||||
@@ -44,47 +41,3 @@ This chatmail provider is run by a small voluntary group of devs and sysadmins,
|
|||||||
who [publically develop chatmail provider setups](https://github.com/deltachat/chatmail).
|
who [publically develop chatmail provider setups](https://github.com/deltachat/chatmail).
|
||||||
Chatmail setups aim to be very low-maintenance, resource efficient and
|
Chatmail setups aim to be very low-maintenance, resource efficient and
|
||||||
interoperable with any other standards-compliant e-mail service.
|
interoperable with any other standards-compliant e-mail service.
|
||||||
///
|
|
||||||
|
|
||||||
/// tab | 🇷🇺 Русский
|
|
||||||
|
|
||||||
## Дополнительная информация
|
|
||||||
|
|
||||||
{{ config.mail_domain }} предоставляет малозатратный, ресурсосберегающий и совместимый с другими системами почтовый сервис для всех. За `chatmail` фактически скрывается
|
|
||||||
обычный почтовый адрес, как и любой другой, но оптимизированный
|
|
||||||
для использования в чатах, особенно DeltaChat.
|
|
||||||
|
|
||||||
### Ограничения по скорости и хранению
|
|
||||||
|
|
||||||
* Незашифрованные сообщения блокируются для получателей вне
|
|
||||||
{{config.mail_domain}}, но добавление контакта через [QR-коды приглашения](https://delta.chat/en/help#howtoe2ee)
|
|
||||||
позволяет свободно обмениваться сообщениями между с ним.
|
|
||||||
|
|
||||||
* Вы можете отправлять до {{ config.max_user_send_per_minute }} сообщений в минуту.
|
|
||||||
|
|
||||||
- Вы можете хранить до [{{ config.max_mailbox_size }} сообщений на сервере](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server).
|
|
||||||
|
|
||||||
* Сообщения в любом случае будут удалены с сервера через {{ config.delete_mails_after }} дней после поступления на сервер.
|
|
||||||
Или раньше, если хранилище превышает допустимый объем.
|
|
||||||
|
|
||||||
### <a name="account-deletion"></a> Удаление аккаунта
|
|
||||||
|
|
||||||
Если вы удалите профиль {{ config.mail_domain }} через приложение Delta Chat,
|
|
||||||
соответствующая учетная запись на сервере и все связанные с ней данные
|
|
||||||
будут автоматически удалены через {{ config.delete_inactive_users_after }} дней.
|
|
||||||
|
|
||||||
Если вы используете несколько устройств,
|
|
||||||
вам необходимо удалить профиль чата на каждом из них,
|
|
||||||
чтобы все данные аккаунта были удалены с сервера.
|
|
||||||
|
|
||||||
Если у вас есть дополнительные вопросы или запросы по поводу удаления аккаунта,
|
|
||||||
пожалуйста, отправьте сообщение со своей учетной записи на {{ config.privacy_mail }}.
|
|
||||||
|
|
||||||
### Кто операторы? Какое ПО используется?
|
|
||||||
|
|
||||||
Этот chatmail провайдер управляется небольшой группой добровольцев — разработчиков и системных администраторов,
|
|
||||||
которые [публично разрабатывают инфраструктуру chatmail провайдеров](https://github.com/deltachat/chatmail).
|
|
||||||
Chatmail стремится быть максимально простыми в обслуживании, ресурсосберегающими и
|
|
||||||
совместимыми с любым другим почтовым сервисом, соответствующим стандартам.
|
|
||||||
|
|
||||||
///
|
|
||||||
|
|||||||
@@ -84,57 +84,3 @@ code {
|
|||||||
color: white !important;
|
color: white !important;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabbed-set {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: 1em 0;
|
|
||||||
border-radius: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbed-set > input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbed-set label {
|
|
||||||
width: auto;
|
|
||||||
padding: 0.9375em 1.25em 0.78125em;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.84em;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-bottom: 0.15rem solid transparent;
|
|
||||||
border-top-left-radius: 0.1rem;
|
|
||||||
border-top-right-radius: 0.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 250ms, color 250ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbed-set .tabbed-content {
|
|
||||||
width: 100%;
|
|
||||||
display: none;
|
|
||||||
box-shadow: 0 -.05rem #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbed-set input {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbed-set input:checked:nth-child(n+1) + label {
|
|
||||||
color: red;
|
|
||||||
border-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen {
|
|
||||||
.tabbed-set input:nth-child(n+1):checked + label + .tabbed-content {
|
|
||||||
order: 99;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.tabbed-content {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
<img class="banner" src="collage-privacy.png"/>
|
|
||||||
|
|
||||||
/// tab | 🇬🇧 English
|
|
||||||
|
|
||||||
# Privacy Policy for {{ config.mail_domain }}
|
# Privacy Policy for {{ config.mail_domain }}
|
||||||
|
|
||||||
@@ -270,199 +267,5 @@ as of *October 2024*.
|
|||||||
Due to the further development of our service and offers
|
Due to the further development of our service and offers
|
||||||
or due to changed legal or official requirements,
|
or due to changed legal or official requirements,
|
||||||
it may become necessary to revise this data protection declaration from time to time.
|
it may become necessary to revise this data protection declaration from time to time.
|
||||||
///
|
|
||||||
|
|
||||||
/// tab | 🇷🇺 Русский
|
|
||||||
|
|
||||||
# Политика конфиденциальности для {{ config.mail_domain }}
|
|
||||||
|
|
||||||
{% if config.mail_domain == "nine.testrun.org" %}
|
|
||||||
Добро пожаловать на `{{config.mail_domain}}` — это основной сервер Chatmail для новых пользователей Delta Chat.
|
|
||||||
Он поддерживается небольшой командой системных администраторов на добровольной основе.
|
|
||||||
Альтернативные сервера вы можете найти [здесь](https://delta.chat/en/chatmail).
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
## Кратко: Личные данные не запрашиваются и не собираются
|
|
||||||
|
|
||||||
Этот сервер Chatmail не запрашивает и не сохраняет личную информацию.
|
|
||||||
Серверы Chatmail существуют исключительно для надёжной передачи (временного хранения и доставки) зашифрованных сообщений между устройствами пользователей, использующих мессенджер Delta Chat.
|
|
||||||
|
|
||||||
Технически, Chatmail-сервер можно представить как «маршрутизатор сообщений» с поддержкой сквозного шифрования в масштабе интернета.
|
|
||||||
|
|
||||||
В отличие от классических почтовых сервисов (например, Gmail),
|
|
||||||
Chatmail-серверы не запрашивают личные данные и не хранят письма постоянно.
|
|
||||||
Они ближе по устройству к серверам Signal,
|
|
||||||
однако не используют номера телефонов и могут безопасно и автоматически взаимодействовать как с другими Chatmail-серверами, так и с обычной электронной почтой.
|
|
||||||
|
|
||||||
Отличия от традиционных почтовых серверов:
|
|
||||||
|
|
||||||
- безусловное удаление сообщений через {{ config.delete_mails_after }} дней;
|
|
||||||
- невозможность отправки незашифрованных сообщений;
|
|
||||||
- отсутствие хранения IP-адресов;
|
|
||||||
- IP-адреса не обрабатываются в связке с адресами электронной почты.
|
|
||||||
|
|
||||||
Из-за отсутствия обработки персональных данных
|
|
||||||
данный сервер, возможно, формально не обязан иметь политику конфиденциальности.
|
|
||||||
|
|
||||||
Тем не менее, ниже приведена юридическая информация
|
|
||||||
для удобства специалистов по защите данных и юристов, изучающих работу Chatmail.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Название и контактная информация
|
|
||||||
|
|
||||||
Ответственный за обработку ваших персональных данных:
|
|
||||||
|
|
||||||
```
|
|
||||||
{{ config.privacy_postal }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Эл. почта: {{ config.privacy_mail }}
|
|
||||||
|
|
||||||
Назначен ответственный по защите данных:
|
|
||||||
|
|
||||||
```
|
|
||||||
{{ config.privacy_pdo }}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Обработка при использовании чата и электронной почты
|
|
||||||
|
|
||||||
Мы предоставляем сервисы, оптимизированные для работы с приложением [Delta Chat](https://delta.chat),
|
|
||||||
и обрабатываем только те данные, которые необходимы для настройки и технической реализации доставки сообщений.
|
|
||||||
Цель обработки — дать пользователям возможность читать, писать, управлять, удалять, отправлять и получать сообщения.
|
|
||||||
|
|
||||||
Для этого мы используем серверное ПО, обеспечивающее передачу сообщений.
|
|
||||||
|
|
||||||
Обрабатываются следующие данные:
|
|
||||||
|
|
||||||
- Исходящие и входящие сообщения (SMTP) временно хранятся до их доставки получателю;
|
|
||||||
- Сообщения доступны получателю через IMAP до их удаления пользователем или по истечении установленного срока
|
|
||||||
(*обычно 4–8 недель*);
|
|
||||||
- Протоколы IMAP и SMTP защищены паролем, уникальным для каждого аккаунта;
|
|
||||||
- Пользователи могут самостоятельно просматривать или удалять сообщения через любой стандартный IMAP-клиент;
|
|
||||||
- Также возможно подключение к «службе передачи в реальном времени»,
|
|
||||||
которая устанавливает P2P-соединение между устройствами и позволяет отправлять временные сообщения,
|
|
||||||
которые *никогда* не сохраняются на сервере — даже в зашифрованном виде.
|
|
||||||
|
|
||||||
### 2.1 Создание аккаунта
|
|
||||||
|
|
||||||
Аккаунт создаётся одним из двух способов:
|
|
||||||
|
|
||||||
- с помощью QR-кода приглашения,
|
|
||||||
отсканированного через приложение Delta Chat;
|
|
||||||
|
|
||||||
- автоматически, при создании и регистрации аккаунта в {{ config.mail_domain }} через приложение Delta Chat.
|
|
||||||
|
|
||||||
В любом случае, обрабатывается только созданный адрес электронной почты.
|
|
||||||
Номера телефонов, другие адреса электронной почты или любые другие идентификаторы не требуются.
|
|
||||||
Правовое основание для обработки —
|
|
||||||
статья 6 (1) пункт b Общего регламента по защите данных (GDPR),
|
|
||||||
так как вы заключаете пользовательский договор, пользуясь нашим сервисом.
|
|
||||||
|
|
||||||
### 2.2 Обработка почтовых сообщений
|
|
||||||
|
|
||||||
Кроме того, мы обрабатываем данные,
|
|
||||||
необходимые для обеспечения стабильной работы инфраструктуры сервера,
|
|
||||||
доставки сообщений и предотвращения злоупотреблений.
|
|
||||||
|
|
||||||
- Поэтому может потребоваться обработка содержимого и/или метаданных
|
|
||||||
(например, заголовков писем и технической информации SMTP) во время передачи;
|
|
||||||
|
|
||||||
- Мы храним логи передаваемых сообщений ограниченное время —
|
|
||||||
они используются для устранения проблем с доставкой и ошибок ПО.
|
|
||||||
|
|
||||||
Также мы вводим ограничения для защиты системы от перегрузок:
|
|
||||||
|
|
||||||
- ограничения скорости (rate limits),
|
|
||||||
- лимиты на объём хранения,
|
|
||||||
- ограничения на размер сообщений,
|
|
||||||
- любые другие меры, необходимые для стабильной работы сервера и предотвращения злоупотреблений.
|
|
||||||
|
|
||||||
Обработка вышеуказанных данных необходима для предоставления сервиса.
|
|
||||||
Правовое основание — статья 6 (1) пункт b GDPR.
|
|
||||||
Обработка данных в целях безопасности и предотвращения злоупотреблений основана на статье 6 (1) пункт f GDPR,
|
|
||||||
и соответствует нашим законным интересам.
|
|
||||||
|
|
||||||
Мы не используем собранные данные для определения вашей личности.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Обработка при посещении сайта
|
|
||||||
|
|
||||||
При посещении нашего сайта браузер вашего устройства
|
|
||||||
автоматически передаёт определённую информацию на сервер,
|
|
||||||
где она временно сохраняется в так называемых лог-файлах.
|
|
||||||
Эти данные автоматически удаляются (обычно через *7 дней*).
|
|
||||||
|
|
||||||
Среди собираемых данных:
|
|
||||||
|
|
||||||
- тип используемого браузера,
|
|
||||||
- операционная система,
|
|
||||||
- дата и время доступа,
|
|
||||||
- страна и IP-адрес,
|
|
||||||
- запрашиваемый файл или ресурс,
|
|
||||||
- объём переданных данных,
|
|
||||||
- статус доступа (успешно, ошибка и т.п.),
|
|
||||||
- страница, с которой был сделан запрос.
|
|
||||||
|
|
||||||
Хостинг нашего сайта осуществляется внешним провайдером.
|
|
||||||
Личные данные, собираемые на сайте, хранятся на его серверах.
|
|
||||||
Провайдер обрабатывает данные строго по нашим инструкциям,
|
|
||||||
в пределах заключённого договора на обработку данных (ст. 28 GDPR).
|
|
||||||
|
|
||||||
Цели обработки:
|
|
||||||
|
|
||||||
- обеспечение стабильного подключения к сайту;
|
|
||||||
- удобство использования сайта;
|
|
||||||
- контроль безопасности и стабильности системы;
|
|
||||||
- административные цели.
|
|
||||||
|
|
||||||
Правовое основание — статья 6 (1) пункт f GDPR.
|
|
||||||
Собранные данные не используются для установления вашей личности.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Передача данных
|
|
||||||
|
|
||||||
Мы не сохраняем личные данные,
|
|
||||||
но письма, ожидающие доставки, могут содержать личную информацию.
|
|
||||||
Такие данные не передаются третьим лицам, за исключением следующих случаев:
|
|
||||||
|
|
||||||
a) при наличии вашего явного согласия (ст. 6 п.1 п. a GDPR);
|
|
||||||
|
|
||||||
b) если передача необходима для защиты прав, интересов или правовой позиции (ст. 6 п.1 п. f GDPR);
|
|
||||||
|
|
||||||
c) если это требуется по закону (ст. 6 п.1 п. c GDPR);
|
|
||||||
|
|
||||||
d) если это необходимо для исполнения договора с вами (ст. 6 п.1 п. b GDPR);
|
|
||||||
|
|
||||||
e) если обработка осуществляется сервис-провайдером по нашему поручению,
|
|
||||||
с которым заключён договор (ст. 28 GDPR),
|
|
||||||
предусматривающий меры безопасности и контроль с нашей стороны.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Права субъектов данных
|
|
||||||
|
|
||||||
Ваши права закреплены в статьях 12–23 GDPR.
|
|
||||||
Так как сервер не хранит персональные данные — даже в зашифрованном виде —
|
|
||||||
предоставление информации или подача возражений не требуются.
|
|
||||||
Удаление данных можно выполнить напрямую через приложение Delta Chat.
|
|
||||||
|
|
||||||
Если у вас есть вопросы или жалобы, напишите нам:
|
|
||||||
{{ config.privacy_mail }}
|
|
||||||
|
|
||||||
Также вы можете обратиться в надзорный орган по месту вашего проживания,
|
|
||||||
работы или к органу, ответственному за нашу деятельность:
|
|
||||||
`{{ config.privacy_supervisor }}`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Актуальность политики конфиденциальности
|
|
||||||
|
|
||||||
Настоящая политика действует с *октября 2024 года*.
|
|
||||||
В случае изменений в услугах или законодательства
|
|
||||||
она может быть обновлена.
|
|
||||||
///
|
|
||||||
|
|||||||
Reference in New Issue
Block a user