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 %}
Note: this is only a temporary development chatmail service
{% endif %} diff --git a/www/src/info.md b/www/src/info.md index 1fd7eb5e3..983d3f13d 100644 --- a/www/src/info.md +++ b/www/src/info.md @@ -1,43 +1,3 @@ + -## 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