diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ea095930..054ad00b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@
- Increase maxproc for reinjecting ports from 10 to 100
([#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
([#639](https://github.com/chatmail/relay/pull/639))
diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py
index ae5f44230..2a4f2a934 100644
--- a/chatmaild/src/chatmaild/config.py
+++ b/chatmaild/src/chatmaild/config.py
@@ -33,6 +33,10 @@ def __init__(self, inipath, params):
self.password_min_length = int(params["password_min_length"])
self.passthrough_senders = params["passthrough_senders"].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.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.filtermail_smtp_port_incoming = int(
diff --git a/chatmaild/src/chatmaild/ini/chatmail.ini.f b/chatmaild/src/chatmaild/ini/chatmail.ini.f
index a99fb508d..ab2c58f4c 100644
--- a/chatmaild/src/chatmaild/ini/chatmail.ini.f
+++ b/chatmaild/src/chatmaild/ini/chatmail.ini.f
@@ -49,6 +49,12 @@
# 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
filtermail_smtp_port = 10080
postfix_reinject_port = 10025
diff --git a/cmdeploy/pyproject.toml b/cmdeploy/pyproject.toml
index 9f2257b56..adf53cf9c 100644
--- a/cmdeploy/pyproject.toml
+++ b/cmdeploy/pyproject.toml
@@ -20,6 +20,7 @@ dependencies = [
"pytest-xdist",
"execnet",
"imap_tools",
+ "pymdown-extensions",
]
[project.scripts]
diff --git a/cmdeploy/src/cmdeploy/www.py b/cmdeploy/src/cmdeploy/www.py
index c013d7414..865f54e54 100644
--- a/cmdeploy/src/cmdeploy/www.py
+++ b/cmdeploy/src/cmdeploy/www.py
@@ -11,6 +11,13 @@
from .genqr import gen_qr_png_data
+LANGUAGE_NAMES = {
+ "EN": " 🇬🇧 English",
+ "RU": " 🇷🇺 Русский",
+ # "UA": "Українська",
+ # "FR": "Français",
+ # "DE": "Deutsch",
+}
def snapshot_dir_stats(somedir):
d = {}
@@ -22,12 +29,59 @@ def snapshot_dir_stats(somedir):
return d
-def prepare_template(source):
- assert source.exists(), source
- render_vars = {}
- render_vars["pagename"] = "home" if source.stem == "index" else source.stem
- render_vars["markdown_html"] = markdown.markdown(source.read_text())
- page_layout = source.with_name("page-layout.html").read_text()
+def prepare_template(source, locales_dir, languages=["EN"]):
+ assert source.exists(), f"Template {source} not found."
+ assert locales_dir.exists(), f"Locales directory {locales_dir} not found."
+ base_name = source.stem
+ render_vars = {
+ "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
@@ -80,6 +134,7 @@ def int_to_english(number):
def _build_webpages(src_dir, build_dir, config):
mail_domain = config.mail_domain
+ languages = config.languages
assert src_dir.exists(), src_dir
if not build_dir.exists():
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.write_bytes(gen_qr_png_data(mail_domain).read())
+ locales_dir = src_dir / "locales"
+
for path in src_dir.iterdir():
if path.suffix == ".md":
- render_vars, content = prepare_template(path)
- 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(
- config.password_min_length
- )
+ render_vars, content = prepare_template(path, locales_dir, languages)
+
+ if render_vars is None:
+ continue
+
+ 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(config.password_min_length)
+
target = build_dir.joinpath(path.stem + ".html")
# recursive jinja2 rendering
@@ -110,9 +166,11 @@ def _build_webpages(src_dir, build_dir, config):
with target.open("w") as f:
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.write_bytes(path.read_bytes())
+
return build_dir
diff --git a/www/src/index.md b/www/src/index.md
index e167c7402..fa3db1712 100644
--- a/www/src/index.md
+++ b/www/src/index.md
@@ -1,29 +1,8 @@
-## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
+%content placeholder%
-{% if config.mail_domain != "nine.testrun.org" %}
-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 %}
-
-Get a {{config.mail_domain}} chat profile
-
-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:
-
-
-
-
-🐣 **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" %}
+{% if config.is_development_instance == True %}
-## More information
-
-{{ 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.
-
-
-### 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.
+%content placeholder%
\ No newline at end of file
diff --git a/www/src/main.css b/www/src/main.css
index 772b2e9d6..6acf12a32 100644
--- a/www/src/main.css
+++ b/www/src/main.css
@@ -84,3 +84,57 @@ code {
color: white !important;
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;
+ }
+}
diff --git a/www/src/privacy.md b/www/src/privacy.md
index a66269605..db20e1a29 100644
--- a/www/src/privacy.md
+++ b/www/src/privacy.md
@@ -1,271 +1,3 @@
+
-# Privacy Policy for {{ config.mail_domain }}
-
-{% 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.
-
-
+%content placeholder%
\ No newline at end of file