www: add markdown language switcher

This commit is contained in:
Keonik1
2025-10-07 21:04:06 +02:00
committed by missytake
parent 0ed7c360a9
commit 62f028bc67
9 changed files with 150 additions and 352 deletions

View File

@@ -14,6 +14,10 @@
- Increase maxproc for reinjecting ports from 10 to 100 - Increase maxproc for reinjecting ports from 10 to 100
([#646](https://github.com/chatmail/relay/pull/646)) ([#646](https://github.com/chatmail/relay/pull/646))
- Add markdown tabs blocks for rendering multilingual pages.
Add russian language support to `index.md`, `privacy.md`, and `info.md`.
([#658](https://github.com/chatmail/relay/pull/658))
- Allow ports 143 and 993 to be used by `dovecot` process - Allow ports 143 and 993 to be used by `dovecot` process
([#639](https://github.com/chatmail/relay/pull/639)) ([#639](https://github.com/chatmail/relay/pull/639))

View File

@@ -33,6 +33,10 @@ 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 = (
params.get("is_development_instance", "true").lower() == "true"
)
self.languages = (params.get("languages", "EN").split())
self.www_folder = params.get("www_folder", "") self.www_folder = params.get("www_folder", "")
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(

View File

@@ -49,6 +49,12 @@ passthrough_recipients = xstore@testrun.org echo@{mail_domain}
# Deployment Details # Deployment Details
# #
# A space-separated list of languages to be displayed on the site.
# Now available languages: EN RU
# You can also use the keyword "ALL"
# NOTE: The order of languages affects their order on the page
languages = EN
# 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

View File

@@ -20,6 +20,7 @@ dependencies = [
"pytest-xdist", "pytest-xdist",
"execnet", "execnet",
"imap_tools", "imap_tools",
"pymdown-extensions",
] ]
[project.scripts] [project.scripts]

View File

@@ -11,6 +11,13 @@ from jinja2 import Template
from .genqr import gen_qr_png_data from .genqr import gen_qr_png_data
LANGUAGE_NAMES = {
"EN": " 🇬🇧 English",
"RU": " 🇷🇺 Русский",
# "UA": "Українська",
# "FR": "Français",
# "DE": "Deutsch",
}
def snapshot_dir_stats(somedir): def snapshot_dir_stats(somedir):
d = {} d = {}
@@ -22,12 +29,59 @@ def snapshot_dir_stats(somedir):
return d return d
def prepare_template(source): def prepare_template(source, locales_dir, languages=["EN"]):
assert source.exists(), source assert source.exists(), f"Template {source} not found."
render_vars = {} assert locales_dir.exists(), f"Locales directory {locales_dir} not found."
render_vars["pagename"] = "home" if source.stem == "index" else source.stem base_name = source.stem
render_vars["markdown_html"] = markdown.markdown(source.read_text()) render_vars = {
page_layout = source.with_name("page-layout.html").read_text() "pagename": "home" if base_name == "index" else base_name
}
selected_langs = (
sorted([d.name.upper() for d in locales_dir.iterdir() if d.is_dir()])
if "ALL" in [l.upper() for l in languages]
else [l.upper() for l in languages]
)
markdown_blocks = []
tabs_enabled = False
if len(selected_langs) > 1:
tabs_enabled = True
for lang_code in selected_langs:
lang_folder = locales_dir / lang_code
lang_file = lang_folder / f"{base_name}.md"
lang_name = LANGUAGE_NAMES.get(lang_code, lang_code)
if lang_file.exists():
content = lang_file.read_text().strip()
else:
print(f"[WARNING]: Missing file {lang_file}. Inserting fallback message.")
content = "Content for this language is not available, please contact your server administrator."
if tabs_enabled:
markdown_blocks.append(f"/// tab | {lang_name}\n{content}\n///")
continue
markdown_blocks.append(content)
if not markdown_blocks:
print("[WARNING] No valid language content found. Skipping file.")
return None, None
original_markdown = source.read_text()
combined_markdown = original_markdown.replace("%content placeholder%", "\n\n".join(markdown_blocks))
render_vars["markdown_html"] = markdown.markdown(
combined_markdown,
extensions=["pymdownx.blocks.tab"]
)
page_layout_path = source.with_name("page-layout.html")
assert page_layout_path.exists(), f"Missing template: {page_layout_path}"
page_layout = page_layout_path.read_text()
return render_vars, page_layout return render_vars, page_layout
@@ -80,6 +134,7 @@ def int_to_english(number):
def _build_webpages(src_dir, build_dir, config): def _build_webpages(src_dir, build_dir, config):
mail_domain = config.mail_domain mail_domain = config.mail_domain
languages = config.languages
assert src_dir.exists(), src_dir assert src_dir.exists(), src_dir
if not build_dir.exists(): if not build_dir.exists():
build_dir.mkdir() build_dir.mkdir()
@@ -87,18 +142,19 @@ def _build_webpages(src_dir, build_dir, config):
qr_path = build_dir.joinpath(f"qr-chatmail-invite-{mail_domain}.png") qr_path = build_dir.joinpath(f"qr-chatmail-invite-{mail_domain}.png")
qr_path.write_bytes(gen_qr_png_data(mail_domain).read()) qr_path.write_bytes(gen_qr_png_data(mail_domain).read())
locales_dir = src_dir / "locales"
for path in src_dir.iterdir(): for path in src_dir.iterdir():
if path.suffix == ".md": if path.suffix == ".md":
render_vars, content = prepare_template(path) render_vars, content = prepare_template(path, locales_dir, languages)
render_vars["username_min_length"] = int_to_english(
config.username_min_length if render_vars is None:
) continue
render_vars["username_max_length"] = int_to_english(
config.username_max_length render_vars["username_min_length"] = int_to_english(config.username_min_length)
) render_vars["username_max_length"] = int_to_english(config.username_max_length)
render_vars["password_min_length"] = int_to_english( render_vars["password_min_length"] = int_to_english(config.password_min_length)
config.password_min_length
)
target = build_dir.joinpath(path.stem + ".html") target = build_dir.joinpath(path.stem + ".html")
# recursive jinja2 rendering # recursive jinja2 rendering
@@ -110,9 +166,11 @@ def _build_webpages(src_dir, build_dir, config):
with target.open("w") as f: with target.open("w") as f:
f.write(content) f.write(content)
elif path.name != "page-layout.html":
elif path.name != "page-layout.html" and path.name != "locales":
target = build_dir.joinpath(path.name) target = build_dir.joinpath(path.name)
target.write_bytes(path.read_bytes()) target.write_bytes(path.read_bytes())
return build_dir return build_dir

View File

@@ -1,29 +1,8 @@
<img class="banner" src="collage-top.png"/> <img class="banner" src="collage-top.png"/>
## Dear [Delta Chat](https://get.delta.chat) users and newcomers ... %content placeholder%
{% if config.mail_domain != "nine.testrun.org" %} {% if config.is_development_instance == True %}
Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :)
{% else %}
Welcome to the default onboarding server ({{ config.mail_domain }})
for Delta Chat users. For details how it avoids storing personal information
please see our [privacy policy](privacy.html).
{% endif %}
<a class="cta-button" href="DCACCOUNT:https://{{ config.mail_domain }}/new">Get a {{config.mail_domain}} chat profile</a>
If you are viewing this page on a different device
without a Delta Chat app,
you can also **scan this QR code** with 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>
🐣 **Choose** your Avatar and Name
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
{% if config.mail_domain != "nine.testrun.org" %}
<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 %}

View File

@@ -1,43 +1,3 @@
<img class="banner" src="collage-info.png"/>
## More information %content placeholder%
{{ config.mail_domain }} provides a low-maintenance, resource efficient and
interoperable e-mail service for everyone. What's behind a `chatmail` is
effectively a normal e-mail address just like any other but optimized
for the usage in chats, especially DeltaChat.
### Rate and storage limits
- Un-encrypted messages are blocked to recipients outside
{{config.mail_domain}} but setting up contact via [QR invite codes](https://delta.chat/en/help#howtoe2ee)
allows your messages to pass freely to any outside recipients.
- You may send up to {{ config.max_user_send_per_minute }} messages per minute.
- You can store up to [{{ config.max_mailbox_size }} messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server).
- Messages are unconditionally removed latest {{ config.delete_mails_after }} days after arriving on the server.
Earlier, if storage may exceed otherwise.
### <a name="account-deletion"></a> Account deletion
If you remove a {{ config.mail_domain }} profile from within the Delta Chat app,
then the according account on the server, along with all associated data,
is automatically deleted {{ config.delete_inactive_users_after }} days afterwards.
If you use multiple devices
then you need to remove the according chat profile from each device
in order for all account data to be removed on the server side.
If you have any further questions or requests regarding account deletion
please send a message from your account to {{ config.privacy_mail }}.
### Who are the operators? Which software is running?
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).
Chatmail setups aim to be very low-maintenance, resource efficient and
interoperable with any other standards-compliant e-mail service.

View File

@@ -84,3 +84,57 @@ 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;
}
}

View File

@@ -1,271 +1,3 @@
<img class="banner" src="collage-privacy.png"/>
# Privacy Policy for {{ config.mail_domain }} %content placeholder%
{% if config.mail_domain == "nine.testrun.org" %}
Welcome to `{{config.mail_domain}}`, the default chatmail onboarding server for Delta Chat users.
It is operated on the side by a small sysops team
on a voluntary basis.
See [other chatmail servers](https://delta.chat/en/chatmail) for alternative server operators.
{% endif %}
## Summary: No personal data asked or collected
This chatmail server neither asks for nor retains personal information.
Chatmail servers exist to reliably transmit (store and deliver) end-to-end encrypted messages
between user's devices running the Delta Chat messenger app.
Technically, you may think of a Chatmail server as
an end-to-end encrypted "messaging router" at Internet-scale.
A chatmail server is very unlike classic e-mail servers (for example Google Mail servers)
that ask for personal data and permanently store messages.
A chatmail server behaves more like the Signal messaging server
but does not know about phone numbers and securely and automatically interoperates
with other chatmail and classic e-mail servers.
Unlike classic e-mail servers, this chatmail server
- unconditionally removes messages after {{ config.delete_mails_after }} days,
- prohibits sending out un-encrypted messages,
- does not store Internet addresses ("IP addresses"),
- does not process IP addresses in relation to email addresses.
Due to the resulting lack of personal data processing
this chatmail server may not require a privacy policy.
Nevertheless, we provide legal details below to make life easier
for data protection specialists and lawyers scrutinizing chatmail operations.
## 1. Name and contact information
Responsible for the processing of your personal data is:
```
{{ config.privacy_postal }}
```
E-mail: {{ config.privacy_mail }}
We have appointed a data protection officer:
```
{{ config.privacy_pdo }}
```
## 2. Processing when using chat e-mail services
We provide services optimized for the use from [Delta Chat](https://delta.chat) apps
and process only the data necessary
for the setup and technical execution of message delivery.
The purpose of the processing is that users can
read, write, manage, delete, send, and receive chat messages.
For this purpose,
we operate server-side software
that enables us to send and receive messages.
We process the following data and details:
- Outgoing and incoming messages (SMTP) are stored for transit
on behalf of their users until the message can be delivered.
- E-Mail-Messages are stored for the recipient and made accessible via IMAP protocols,
until explicitly deleted by the user or until a fixed time period is exceeded,
(*usually 4-8 weeks*).
- IMAP and SMTP protocols are password protected with unique credentials for each account.
- Users can retrieve or delete all stored messages
without intervention from the operators using standard IMAP client tools.
- Users can connect to a "realtime relay service"
to establish Peer-to-Peer connection between user devices,
allowing them to send and retrieve ephemeral messages
which are never stored on the chatmail server, also not in encrypted form.
### 2.1 Account setup
Creating an account happens in one of two ways on our mail servers:
- with a QR invitation token
which is scanned using the Delta Chat app
and then the account is created.
- by letting Delta Chat otherwise create an account
and register it with a {{ config.mail_domain }} mail server.
In either case, we process the newly created email address.
No phone numbers,
other email addresses,
or other identifiable data
is currently required.
The legal basis for the processing is
Art. 6 (1) lit. b GDPR,
as you have a usage contract with us
by using our services.
### 2.2 Processing of E-Mail-Messages
In addition,
we will process data
to keep the server infrastructure operational
for purposes of e-mail dispatch
and abuse prevention.
- Therefore,
it is necessary to process the content and/or metadata
(e.g., headers of the email as well as smtp chatter)
of E-Mail-Messages in transit.
- We will keep logs of messages in transit for a limited time.
These logs are used to debug delivery problems and software bugs.
In addition,
we process data to protect the systems from excessive use.
Therefore, limits are enforced:
- rate limits
- storage limits
- message size limits
- any other limit necessary for the whole server to function in a healthy way
and to prevent abuse.
The processing and use of the above permissions
are performed to provide the service.
The data processing is necessary for the use of our services,
therefore the legal basis of the processing is
Art. 6 (1) lit. b GDPR,
as you have a usage contract with us
by using our services.
The legal basis for the data processing
for the purposes of security and abuse prevention is
Art. 6 (1) lit. f GDPR.
Our legitimate interest results
from the aforementioned purposes.
We will not use the collected data
for the purpose of drawing conclusions
about your person.
## 3. Processing when using our Website
When you visit our website,
the browser used on your end device
automatically sends information to the server of our website.
This information is temporarily stored in a so-called log file.
The following information is collected and stored
until it is automatically deleted
(*usually 7 days*):
- used type of browser,
- used operating system,
- access date and time as well as
- country of origin and IP address,
- the requested file name or HTTP resource,
- the amount of data transferred,
- the access status (file transferred, file not found, etc.) and
- the page from which the file was requested.
This website is hosted by an external service provider (hoster).
The personal data collected on this website is stored
on the hoster's servers.
Our hoster will process your data
only to the extent necessary to fulfill its obligations
to perform under our instructions.
In order to ensure data protection-compliant processing,
we have concluded a data processing agreement with our hoster.
The aforementioned data is processed by us for the following purposes:
- Ensuring a reliable connection setup of the website,
- ensuring a convenient use of our website,
- checking and ensuring system security and stability, and
- for other administrative purposes.
The legal basis for the data processing is
Art. 6 (1) lit. f GDPR.
Our legitimate interest results
from the aforementioned purposes of data collection.
We will not use the collected data
for the purpose of drawing conclusions about your person.
## 4. Transfer of Data
We do not retain any personal data but e-mail messages waiting to be delivered
may contain personal data.
Any such residual personal data will not be transferred to third parties
for purposes other than those listed below:
a) you have given your express consent
in accordance with Art. 6 para. 1 sentence 1 lit. a GDPR,
b) the disclosure is necessary for the assertion, exercise or defence of legal claims
pursuant to Art. 6 (1) sentence 1 lit. f GDPR
and there is no reason to assume that you have
an overriding interest worthy of protection
in the non-disclosure of your data,
c) in the event that there is a legal obligation to disclose your data
pursuant to Art. 6 para. 1 sentence 1 lit. c GDPR,
as well as
d) this is legally permissible and necessary
in accordance with Art. 6 Para. 1 S. 1 lit. b GDPR
for the processing of contractual relationships with you,
e) this is carried out by a service provider
acting on our behalf and on our exclusive instructions,
whom we have carefully selected (Art. 28 (1) GDPR)
and with whom we have concluded a corresponding contract on commissioned processing (Art. 28 (3) GDPR),
which obliges our contractor,
among other things,
to implement appropriate security measures
and grants us comprehensive control powers.
## 5. Rights of the data subject
The rights arise from Articles 12 to 23 GDPR.
Since no personal data is stored on our servers,
even in encrypted form,
there is no need to provide information
on these or possible objections.
A deletion can be made
directly in the Delta Chat email messenger.
If you have any questions or complaints,
please feel free to contact us by email:
{{ config.privacy_mail }}
As a rule, you can contact the supervisory authority of your usual place of residence
or workplace
or our registered office for this purpose.
The supervisory authority responsible for our place of business
is the `{{ config.privacy_supervisor }}`.
## 6. Validity of this privacy policy
This data protection declaration is valid
as of *October 2024*.
Due to the further development of our service and offers
or due to changed legal or official requirements,
it may become necessary to revise this data protection declaration from time to time.