From 5b2b7b0294f7a973f6b3afb10f0b2739f653a56e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 25 Aug 2024 15:52:41 +0200 Subject: [PATCH 01/26] Parse error #1712 --- snappymail/v/0.0.0/app/localization/zh-TW/user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/localization/zh-TW/user.json b/snappymail/v/0.0.0/app/localization/zh-TW/user.json index e1a588e2a..73721653c 100644 --- a/snappymail/v/0.0.0/app/localization/zh-TW/user.json +++ b/snappymail/v/0.0.0/app/localization/zh-TW/user.json @@ -310,7 +310,7 @@ "STORE_PRIVATE_KEY_IN_GNUPG": "以 GnuPG 儲存私鑰在伺服器上", "BACKUP_ON_SERVER": "在伺服器中儲存 (已加密)", "BACKUP_PUBLIC_KEY_ON_SERVER": "在伺服器中儲存公鑰", - "BACKUP_PRIVATE_KEY_ON_SERVER": "在伺服器中儲存私鑰", + "BACKUP_PRIVATE_KEY_ON_SERVER": "在伺服器中儲存私鑰" }, "SMIME": { "POPUP_IMPORT_TITLE": "匯入 S\/MIME 證書", From 2ac71fe4ade8023db90a3d27607ed27249b7d881 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 25 Aug 2024 15:54:37 +0200 Subject: [PATCH 02/26] Localization strings for ACL #157 --- CHANGELOG.md | 25 +++++++++++++++++ .../v/0.0.0/app/localization/ar/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/be/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/bg/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/cs/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/da/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/de/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/el/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/en/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/es/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/et/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/eu/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/fa/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/fi/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/fr/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/hu/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/id/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/is/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/it/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/ja/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/ko/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/lt/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/lv/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/nb/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/nl/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/pl/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/pt-BR/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/pt/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/ro/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/ru/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/sk/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/sl/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/sv/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/tr/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/uk/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/vi/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/zh-TW/user.json | 15 ++++++++-- .../v/0.0.0/app/localization/zh/user.json | 15 ++++++++-- .../templates/Views/User/PopupsFolderACL.html | 28 +++++++++---------- 39 files changed, 520 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b869a4179..2b9bcd2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## 2.37.3 – 2024-08-13 + +### Added +- Edit ACL rules + [#157](https://github.com/the-djmaze/snappymail/issues/157) +- Change minimum new mail check interval + [#1678](https://github.com/the-djmaze/snappymail/issues/1678) +- Sieve editor does not support "index" extension + [#1709](https://github.com/the-djmaze/snappymail/issues/1709) + +### Changed +- Improved domain autoconfig interaction +- Simplify sieve scripts list + [#1675](https://github.com/the-djmaze/snappymail/issues/1675) +- Handling of (token) errors due to + [#1706](https://github.com/the-djmaze/snappymail/issues/1706) +- Update Chinese by @Artoria2e5 + +### Fixed +- Thread sorting visible after disabling the imap capability + [#1574](https://github.com/the-djmaze/snappymail/issues/1574) +- Creating new message impossible as long as a draft exists? + [#1710](https://github.com/the-djmaze/snappymail/issues/1710) + + ## 2.37.2 – 2024-08-13 ### Added diff --git a/snappymail/v/0.0.0/app/localization/ar/user.json b/snappymail/v/0.0.0/app/localization/ar/user.json index 9b6720b4f..e6ecc4ad6 100644 --- a/snappymail/v/0.0.0/app/localization/ar/user.json +++ b/snappymail/v/0.0.0/app/localization/ar/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "المهملات", "ARCHIVE_NAME": "الأرشيف" }, - "QUOTA": { - "TITLE": "Quota usage" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "أعد تحميل قائمة الرسائل", diff --git a/snappymail/v/0.0.0/app/localization/be/user.json b/snappymail/v/0.0.0/app/localization/be/user.json index 13de8161e..7ad8e2544 100644 --- a/snappymail/v/0.0.0/app/localization/be/user.json +++ b/snappymail/v/0.0.0/app/localization/be/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Сметніца", "ARCHIVE_NAME": "Архіў" }, - "QUOTA": { - "TITLE": "Дыскавая квота" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Абнавіць Спіс Лістоў", diff --git a/snappymail/v/0.0.0/app/localization/bg/user.json b/snappymail/v/0.0.0/app/localization/bg/user.json index 8cef8ae3e..55c1035e6 100644 --- a/snappymail/v/0.0.0/app/localization/bg/user.json +++ b/snappymail/v/0.0.0/app/localization/bg/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Кошче", "ARCHIVE_NAME": "Архив" }, - "QUOTA": { - "TITLE": "Използване по дялове" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Презареди списъка със съобщения", diff --git a/snappymail/v/0.0.0/app/localization/cs/user.json b/snappymail/v/0.0.0/app/localization/cs/user.json index 400018c7c..faac7fc0c 100644 --- a/snappymail/v/0.0.0/app/localization/cs/user.json +++ b/snappymail/v/0.0.0/app/localization/cs/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Koš", "ARCHIVE_NAME": "Archiv" }, - "QUOTA": { - "TITLE": "Využití kvóty" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Obnovit seznam zpráv", diff --git a/snappymail/v/0.0.0/app/localization/da/user.json b/snappymail/v/0.0.0/app/localization/da/user.json index 766ec5635..03ffa43e2 100644 --- a/snappymail/v/0.0.0/app/localization/da/user.json +++ b/snappymail/v/0.0.0/app/localization/da/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Slettede meddelelser", "ARCHIVE_NAME": "Arkiverede meddelelser" }, - "QUOTA": { - "TITLE": "Forbrug" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Genindlæs meddelelser", diff --git a/snappymail/v/0.0.0/app/localization/de/user.json b/snappymail/v/0.0.0/app/localization/de/user.json index 6b282a718..952536106 100644 --- a/snappymail/v/0.0.0/app/localization/de/user.json +++ b/snappymail/v/0.0.0/app/localization/de/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Papierkorb", "ARCHIVE_NAME": "Archiv" }, - "QUOTA": { - "TITLE": "Speicherplatz-Nutzung" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Nachrichtenliste neu laden", diff --git a/snappymail/v/0.0.0/app/localization/el/user.json b/snappymail/v/0.0.0/app/localization/el/user.json index 02274b7a2..02bdecfe3 100644 --- a/snappymail/v/0.0.0/app/localization/el/user.json +++ b/snappymail/v/0.0.0/app/localization/el/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Άχρηστα", "ARCHIVE_NAME": "Αρχείο" }, - "QUOTA": { - "TITLE": "Χρήση αποθηκευτικού χώρου" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Φόρτωσε ξανά τη λίστα των μηνυμάτων", diff --git a/snappymail/v/0.0.0/app/localization/en/user.json b/snappymail/v/0.0.0/app/localization/en/user.json index bf010228a..44bf9b67c 100644 --- a/snappymail/v/0.0.0/app/localization/en/user.json +++ b/snappymail/v/0.0.0/app/localization/en/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Trash", "ARCHIVE_NAME": "Archive" }, - "QUOTA": { - "TITLE": "Quota usage" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Reload Message List", diff --git a/snappymail/v/0.0.0/app/localization/es/user.json b/snappymail/v/0.0.0/app/localization/es/user.json index ef6a43873..08e327d27 100644 --- a/snappymail/v/0.0.0/app/localization/es/user.json +++ b/snappymail/v/0.0.0/app/localization/es/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Papelera", "ARCHIVE_NAME": "Archivo" }, - "QUOTA": { - "TITLE": "Cuota" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Actualizar lista de mensajes", diff --git a/snappymail/v/0.0.0/app/localization/et/user.json b/snappymail/v/0.0.0/app/localization/et/user.json index 623c7e00c..e6d17f204 100644 --- a/snappymail/v/0.0.0/app/localization/et/user.json +++ b/snappymail/v/0.0.0/app/localization/et/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Prügikast", "ARCHIVE_NAME": "Arhiiv" }, - "QUOTA": { - "TITLE": "Andmemaht" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Laadi kirjade nimekiri uuesti", diff --git a/snappymail/v/0.0.0/app/localization/eu/user.json b/snappymail/v/0.0.0/app/localization/eu/user.json index c41dd2f1f..008449b37 100644 --- a/snappymail/v/0.0.0/app/localization/eu/user.json +++ b/snappymail/v/0.0.0/app/localization/eu/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Zakarrontzia", "ARCHIVE_NAME": "Artxiboa" }, - "QUOTA": { - "TITLE": "Kuota" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Eguneratu mezuen zerrenda", diff --git a/snappymail/v/0.0.0/app/localization/fa/user.json b/snappymail/v/0.0.0/app/localization/fa/user.json index 56c9066ec..e11831efc 100644 --- a/snappymail/v/0.0.0/app/localization/fa/user.json +++ b/snappymail/v/0.0.0/app/localization/fa/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "سطل آشغال", "ARCHIVE_NAME": "بایگانی" }, - "QUOTA": { - "TITLE": "سهمیه استفاده شده" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "بازنگری لیست پیام‌ها", diff --git a/snappymail/v/0.0.0/app/localization/fi/user.json b/snappymail/v/0.0.0/app/localization/fi/user.json index e12835132..94606bf84 100644 --- a/snappymail/v/0.0.0/app/localization/fi/user.json +++ b/snappymail/v/0.0.0/app/localization/fi/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Roskakori", "ARCHIVE_NAME": "Arkisto" }, - "QUOTA": { - "TITLE": "Kiintiö" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Päivitä viestit", diff --git a/snappymail/v/0.0.0/app/localization/fr/user.json b/snappymail/v/0.0.0/app/localization/fr/user.json index eb143004d..060808481 100644 --- a/snappymail/v/0.0.0/app/localization/fr/user.json +++ b/snappymail/v/0.0.0/app/localization/fr/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Corbeille", "ARCHIVE_NAME": "Archive" }, - "QUOTA": { - "TITLE": "Quota" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Recharger la liste des messages", diff --git a/snappymail/v/0.0.0/app/localization/hu/user.json b/snappymail/v/0.0.0/app/localization/hu/user.json index 27848297a..de51b1f8b 100644 --- a/snappymail/v/0.0.0/app/localization/hu/user.json +++ b/snappymail/v/0.0.0/app/localization/hu/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Lomtár", "ARCHIVE_NAME": "Archívum" }, - "QUOTA": { - "TITLE": "Helyfoglalás" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Üzenetlista újratöltés", diff --git a/snappymail/v/0.0.0/app/localization/id/user.json b/snappymail/v/0.0.0/app/localization/id/user.json index c0ae6d070..b08590901 100644 --- a/snappymail/v/0.0.0/app/localization/id/user.json +++ b/snappymail/v/0.0.0/app/localization/id/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Sampah", "ARCHIVE_NAME": "Arsip" }, - "QUOTA": { - "TITLE": "Jatah digunakan" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Muat Ulang Daftar Pesan", diff --git a/snappymail/v/0.0.0/app/localization/is/user.json b/snappymail/v/0.0.0/app/localization/is/user.json index 259cc5155..4eb403353 100644 --- a/snappymail/v/0.0.0/app/localization/is/user.json +++ b/snappymail/v/0.0.0/app/localization/is/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Ruslafata", "ARCHIVE_NAME": "Safnskrá" }, - "QUOTA": { - "TITLE": "Disknotkun" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Endurhlaða skilaboðalista", diff --git a/snappymail/v/0.0.0/app/localization/it/user.json b/snappymail/v/0.0.0/app/localization/it/user.json index d8360c08e..d365bbacd 100644 --- a/snappymail/v/0.0.0/app/localization/it/user.json +++ b/snappymail/v/0.0.0/app/localization/it/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Cestino", "ARCHIVE_NAME": "Archivio" }, - "QUOTA": { - "TITLE": "Uso della quota" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Aggiorna", diff --git a/snappymail/v/0.0.0/app/localization/ja/user.json b/snappymail/v/0.0.0/app/localization/ja/user.json index 8391284f1..4421a531d 100644 --- a/snappymail/v/0.0.0/app/localization/ja/user.json +++ b/snappymail/v/0.0.0/app/localization/ja/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "ごみ箱", "ARCHIVE_NAME": "アーカイブ" }, - "QUOTA": { - "TITLE": "クォータ使用量" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "リスト更新", diff --git a/snappymail/v/0.0.0/app/localization/ko/user.json b/snappymail/v/0.0.0/app/localization/ko/user.json index 746b789b0..39fbcafa3 100644 --- a/snappymail/v/0.0.0/app/localization/ko/user.json +++ b/snappymail/v/0.0.0/app/localization/ko/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "휴지통", "ARCHIVE_NAME": "보관함" }, - "QUOTA": { - "TITLE": "사용량" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "메일 목록 새로고침", diff --git a/snappymail/v/0.0.0/app/localization/lt/user.json b/snappymail/v/0.0.0/app/localization/lt/user.json index 47f7627b2..24fce3bc6 100644 --- a/snappymail/v/0.0.0/app/localization/lt/user.json +++ b/snappymail/v/0.0.0/app/localization/lt/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Šiukšlynas", "ARCHIVE_NAME": "Archyvas" }, - "QUOTA": { - "TITLE": "Kvotos naudojimas" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Atnaujinti žinučių sąrašą", diff --git a/snappymail/v/0.0.0/app/localization/lv/user.json b/snappymail/v/0.0.0/app/localization/lv/user.json index 178b5e5f1..4fddba713 100644 --- a/snappymail/v/0.0.0/app/localization/lv/user.json +++ b/snappymail/v/0.0.0/app/localization/lv/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Miskaste", "ARCHIVE_NAME": "Arhīvs" }, - "QUOTA": { - "TITLE": "Kvotas izmantojums" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Pārlādēt ziņojumu sarakstu", diff --git a/snappymail/v/0.0.0/app/localization/nb/user.json b/snappymail/v/0.0.0/app/localization/nb/user.json index d164cd8b5..1b65e91fd 100644 --- a/snappymail/v/0.0.0/app/localization/nb/user.json +++ b/snappymail/v/0.0.0/app/localization/nb/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Papirkurv", "ARCHIVE_NAME": "Arkiv" }, - "QUOTA": { - "TITLE": "Kvotebruk" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Hent nye meldinger", diff --git a/snappymail/v/0.0.0/app/localization/nl/user.json b/snappymail/v/0.0.0/app/localization/nl/user.json index 498f223eb..bc4e95b09 100644 --- a/snappymail/v/0.0.0/app/localization/nl/user.json +++ b/snappymail/v/0.0.0/app/localization/nl/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Verwijderde items", "ARCHIVE_NAME": "Archief" }, - "QUOTA": { - "TITLE": "Quota gebruik" + "IMAP": { + "ACL_RIGHTS": "Rechten", + "ACL_A": "Beheren", + "ACL_E": "Uitwissen", + "ACL_I": "Berichten toevoegen", + "ACL_K": "Submappen maken", + "ACL_L": "Toon berichten", + "ACL_P": "Verstuur mail", + "ACL_R": "Map selecteren", + "ACL_S": "\\SEEN label", + "ACL_T": "\\DELETED label", + "ACL_W": "Andere labels", + "ACL_X": "Map verwijderen\/hernoemen" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Berichten controleren", diff --git a/snappymail/v/0.0.0/app/localization/pl/user.json b/snappymail/v/0.0.0/app/localization/pl/user.json index 3e63991fa..e60028201 100644 --- a/snappymail/v/0.0.0/app/localization/pl/user.json +++ b/snappymail/v/0.0.0/app/localization/pl/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Kosz", "ARCHIVE_NAME": "Archiwum" }, - "QUOTA": { - "TITLE": "Wykorzystane miejsce" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Odśwież listę wiadomości", diff --git a/snappymail/v/0.0.0/app/localization/pt-BR/user.json b/snappymail/v/0.0.0/app/localization/pt-BR/user.json index 5d19eef57..bad9a4578 100644 --- a/snappymail/v/0.0.0/app/localization/pt-BR/user.json +++ b/snappymail/v/0.0.0/app/localization/pt-BR/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Lixeira", "ARCHIVE_NAME": "Arquivadas" }, - "QUOTA": { - "TITLE": "Uso do Armazenamento" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Atualizar Lista de Mensagens", diff --git a/snappymail/v/0.0.0/app/localization/pt/user.json b/snappymail/v/0.0.0/app/localization/pt/user.json index 9f6ba6b8b..877f8287d 100644 --- a/snappymail/v/0.0.0/app/localization/pt/user.json +++ b/snappymail/v/0.0.0/app/localization/pt/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Lixo", "ARCHIVE_NAME": "Arquivo" }, - "QUOTA": { - "TITLE": "Uso da Cota" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Atualizar lista de mensagens", diff --git a/snappymail/v/0.0.0/app/localization/ro/user.json b/snappymail/v/0.0.0/app/localization/ro/user.json index e1e47ef52..665c1ce51 100644 --- a/snappymail/v/0.0.0/app/localization/ro/user.json +++ b/snappymail/v/0.0.0/app/localization/ro/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Șterse", "ARCHIVE_NAME": "Archive" }, - "QUOTA": { - "TITLE": "Cotă" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Actualizare", diff --git a/snappymail/v/0.0.0/app/localization/ru/user.json b/snappymail/v/0.0.0/app/localization/ru/user.json index 67ed5ec22..37bcd5f51 100644 --- a/snappymail/v/0.0.0/app/localization/ru/user.json +++ b/snappymail/v/0.0.0/app/localization/ru/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Корзина", "ARCHIVE_NAME": "Архив" }, - "QUOTA": { - "TITLE": "Дисковая квота" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Обновить Список Писем", diff --git a/snappymail/v/0.0.0/app/localization/sk/user.json b/snappymail/v/0.0.0/app/localization/sk/user.json index 1fdd6b29f..7e3a39d15 100644 --- a/snappymail/v/0.0.0/app/localization/sk/user.json +++ b/snappymail/v/0.0.0/app/localization/sk/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Kôš", "ARCHIVE_NAME": "Archív" }, - "QUOTA": { - "TITLE": "Spotreba kvóty" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Znovu načítať zoznam správ", diff --git a/snappymail/v/0.0.0/app/localization/sl/user.json b/snappymail/v/0.0.0/app/localization/sl/user.json index a10963da9..f0c49e6fa 100644 --- a/snappymail/v/0.0.0/app/localization/sl/user.json +++ b/snappymail/v/0.0.0/app/localization/sl/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Smeti", "ARCHIVE_NAME": "Arhiv" }, - "QUOTA": { - "TITLE": "Poraba prostora" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Ponovno naloži seznam sporočil", diff --git a/snappymail/v/0.0.0/app/localization/sv/user.json b/snappymail/v/0.0.0/app/localization/sv/user.json index 140d2ab90..b6ae83b90 100644 --- a/snappymail/v/0.0.0/app/localization/sv/user.json +++ b/snappymail/v/0.0.0/app/localization/sv/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Papperskorg", "ARCHIVE_NAME": "Arkiv" }, - "QUOTA": { - "TITLE": "Kvotanvändning" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Ladda meddelandelista", diff --git a/snappymail/v/0.0.0/app/localization/tr/user.json b/snappymail/v/0.0.0/app/localization/tr/user.json index 9b9857a93..ba422f154 100644 --- a/snappymail/v/0.0.0/app/localization/tr/user.json +++ b/snappymail/v/0.0.0/app/localization/tr/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Çöp Kutusu", "ARCHIVE_NAME": "Arşiv" }, - "QUOTA": { - "TITLE": "Kota Kullanımı" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Mesaj Listesini Yenile", diff --git a/snappymail/v/0.0.0/app/localization/uk/user.json b/snappymail/v/0.0.0/app/localization/uk/user.json index 57ab73118..2258a8cbf 100644 --- a/snappymail/v/0.0.0/app/localization/uk/user.json +++ b/snappymail/v/0.0.0/app/localization/uk/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Кошик", "ARCHIVE_NAME": "Архів" }, - "QUOTA": { - "TITLE": "Дискова квота" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Оновити список листів", diff --git a/snappymail/v/0.0.0/app/localization/vi/user.json b/snappymail/v/0.0.0/app/localization/vi/user.json index 7ddca21a9..1761aa08f 100644 --- a/snappymail/v/0.0.0/app/localization/vi/user.json +++ b/snappymail/v/0.0.0/app/localization/vi/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "Thùng rác", "ARCHIVE_NAME": "Kho lưu trữ" }, - "QUOTA": { - "TITLE": "Dung lượng sử dụng" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Tải lại danh sách thư", diff --git a/snappymail/v/0.0.0/app/localization/zh-TW/user.json b/snappymail/v/0.0.0/app/localization/zh-TW/user.json index 73721653c..a014582dc 100644 --- a/snappymail/v/0.0.0/app/localization/zh-TW/user.json +++ b/snappymail/v/0.0.0/app/localization/zh-TW/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "垃圾桶", "ARCHIVE_NAME": "封存" }, - "QUOTA": { - "TITLE": "配額使用" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "重新載入郵件清單", diff --git a/snappymail/v/0.0.0/app/localization/zh/user.json b/snappymail/v/0.0.0/app/localization/zh/user.json index 16775b061..753092ec6 100644 --- a/snappymail/v/0.0.0/app/localization/zh/user.json +++ b/snappymail/v/0.0.0/app/localization/zh/user.json @@ -88,8 +88,19 @@ "TRASH_NAME": "已删除邮件", "ARCHIVE_NAME": "存档" }, - "QUOTA": { - "TITLE": "存储空间使用量" + "IMAP": { + "ACL_RIGHTS": "Rights", + "ACL_A": "Administer", + "ACL_E": "Expunge", + "ACL_I": "Insert messages", + "ACL_K": "Create sub-folders", + "ACL_L": "Show messages", + "ACL_P": "Send mail", + "ACL_R": "Select folder", + "ACL_S": "\\SEEN tag", + "ACL_T": "\\DELETED tag", + "ACL_W": "Other tags", + "ACL_X": "Delete\/Rename folder" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "刷新消息列表", diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsFolderACL.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsFolderACL.html index b72e095b3..17ee2f43e 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsFolderACL.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsFolderACL.html @@ -11,62 +11,62 @@
- + - + - + - + - + - + - + - + - + - + - + - +
Administer rights
Select folder
Show messages
Insert messages
\SEEN flag
\DELETED flag
Other flags
Send mail
Create sub-folders
Delete/Rename folder
Expunge
From 9c0d31997ab7ab16699a47a31734d6961373c675 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 25 Aug 2024 15:56:11 +0200 Subject: [PATCH 03/26] Fix CRLF #1712 --- .../v/0.0.0/app/localization/zh-TW/admin.json | 364 +++++++++--------- 1 file changed, 182 insertions(+), 182 deletions(-) diff --git a/snappymail/v/0.0.0/app/localization/zh-TW/admin.json b/snappymail/v/0.0.0/app/localization/zh-TW/admin.json index a5f0b57ab..135c091d6 100644 --- a/snappymail/v/0.0.0/app/localization/zh-TW/admin.json +++ b/snappymail/v/0.0.0/app/localization/zh-TW/admin.json @@ -1,182 +1,182 @@ -{ - "GLOBAL": { - "ADD": "新增", - "CLOSE": "關閉", - "SAVE": "儲存", - "TEST": "測試", - "UPDATE": "更新", - "USERNAME": "使用者名稱", - "PASSWORD": "密碼", - "CANCEL": "取消", - "SEARCH": "搜尋" - }, - "LOGIN": { - "LABEL_LOGIN": "登入", - "LABEL_TOTP": "TOTP 密碼", - "BUTTON_LOGIN": "登入到控制面板" - }, - "TOP_PANEL": { - "LABEL_ADMIN_PANEL": "控制面板" - }, - "SETTINGS_LABELS": { - "GENERAL": "常規", - "LOGIN": "登入", - "BRANDING": "Logo", - "CONTACTS": "聯絡人", - "DOMAINS": "域名", - "SECURITY": "安全", - "PACKAGES": "外掛包", - "CONFIG": "配置", - "ABOUT": "關於" - }, - "TAB_GENERAL": { - "LEGEND_INTERFACE": "介面", - "LABEL_LANGUAGE": "語言", - "LABEL_LANGUAGE_ADMIN": "語言(後臺)", - "LABEL_THEME": "主題", - "LABEL_ALLOW_LANGUAGES_ON_SETTINGS": "允許在設定介面中選擇語言", - "LABEL_ALLOW_THEMES_ON_SETTINGS": "允許在設定介面中選擇主題", - "LABEL_ALLOW_BACKGROUND_ON_SETTINGS": "允許在設定介面中選擇背景", - "LABEL_SHOW_THUMBNAILS": "顯示縮圖(附件)", - "LEGEND_MAIN": "主要設定", - "LABEL_ATTACHMENT_SIZE_LIMIT": "附件大小限制", - "LABEL_ALLOW_ADDITIONAL_ACCOUNTS": "允許額外的賬戶", - "LABEL_ALLOW_IDENTITIES": "允許多重身份", - "ALERT_DATA_ACCESS": "資料資料夾可被訪問。請配置您的 Web 伺服器以阻止從外部訪問資料資料夾。獲取更多幫助:", - "ALERT_WARNING": "警告!", - "HTML_ALERT_WEAK_PASSWORD": "您正在使用預設的管理員密碼\n
\n安全起見,請立即將密碼\n更改<\/a><\/strong>\n為其他的字串。\n" - }, - "TAB_LOGIN": { - "LEGEND_LOGIN_SCREEN": "登入介面", - "LABEL_DEFAULT_DOMAIN": "預設域名", - "LABEL_DETERMINE_USER_DOMAIN": "嘗試判斷使用者使用的域名", - "LABEL_ALLOW_LANGUAGES_ON_LOGIN": "允許在登入介面選擇語言", - "LABEL_DETERMINE_USER_LANGUAGE": "嘗試判斷使用者使用語言" - }, - "TAB_BRANDING": { - "LEGEND_BRANDING": "Logo", - "LABEL_PAGE_TITLE": "頁面標題", - "LABEL_LOADING_DESCRIPTION": "載入介面文字", - "LABEL_FAVICON_URL": "網站圖示" - }, - "TAB_CONTACTS": { - "LEGEND_CONTACTS": "聯絡人", - "LEGEND_STORAGE": "儲存(PDO)", - "LABEL_ENABLE_CONTACTS": "啟用聯絡人", - "LABEL_ALLOW_SYNC": "允許同步聯絡人(依靠外部CardDAV伺服器)", - "LABEL_STORAGE_TYPE": "型別", - "LABEL_STORAGE_DSN": "Dsn", - "LABEL_STORAGE_USER": "使用者", - "LABEL_STORAGE_PASSWORD": "密碼", - "SQLITE_GLOBAL": "使用全域性資料庫替代各賬號的資料庫", - "ALERT_NOTICE": "提示!", - "SUGGESTIONS_LIMIT": "建議數量上限", - "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "如果有大量的活躍使用者,請不要選擇此資料庫型別。", - "HTML_ALERT_DOES_NOT_SUPPORTED": "您的系統不支援聯絡人\n
\n您需要在您的伺服器上安裝或啟用PDO (SQLite \/ MySQL \/ PostgreSQL)<\/strong> 元件。\n" - }, - "TAB_DOMAINS": { - "LEGEND_DOMAINS": "域名", - "BUTTON_ADD_DOMAIN": "新增域名", - "BUTTON_ADD_ALIAS": "新增別名", - "DELETE_ARE_YOU_SURE": "您確定嗎?", - "HTML_DOMAINS_HELPER": "允許訪問的域名列表。\n
\n點選域名進行設定。\n" - }, - "TAB_SECURITY": { - "LEGEND_SECURITY": "安全", - "LABEL_USE_IMAGE_PROXY": "對外部圖片使用本地代理", - "LABEL_AUTO_VERIFY": "自動驗證簽名", - "LABEL_SHOW_PHP_INFO": "顯示 PHP 資訊", - "LEGEND_ADMIN_PANEL_ACCESS_CREDENTIALS": "管理員面板訪問憑據", - "LABEL_CURRENT_PASSWORD": "當前密碼", - "LABEL_NEW_PASSWORD": "新密碼", - "LABEL_REPEAT_PASSWORD": "重複輸入新密碼", - "LABEL_REQUIRE_VERIFICATION": "要求驗證 SSL 證書", - "CRYPTO": "加密", - "LABEL_ALLOW_SELF_SIGNED": "允許自簽名證書" - }, - "TAB_PACKAGES": { - "LABEL_ENABLE_PLUGINS": "啟用外掛", - "LEGEND_AVAILABLE_FOR_UPDATE": "有更新可用", - "LEGEND_AVAILABLE_FOR_INSTALLATION": "可用於安裝", - "LEGEND_INSTALLED_PACKAGES": "已安裝的外掛包", - "ALERT_CANNOT_ACCESS_REPOSITORY": "目前無法訪問外掛倉庫" - }, - "TAB_ABOUT": { - "LEGEND_ABOUT": "關於", - "LABEL_TAG_HINT": "簡潔、現代化、快速的網頁郵件客戶端", - "LABEL_ALL_RIGHTS_RESERVED": "保留所有權利", - "HINT_READ_CHANGE_LOG": "請在更新前閱讀更新日誌", - "HINT_IS_UP_TO_DATE": "SnappyMail已是最新版本", - "HTML_NEW_VERSION": "有新的%VERSION%<\/b> 版本可用.", - "LABEL_UPDATING": "更新中", - "LABEL_CHECKING": "檢查更新", - "BUTTON_RELEASES": "發行版" - }, - "POPUPS_DOMAIN_ALIAS": { - "TITLE_ADD_DOMAIN_ALIAS": "新增別名", - "LABEL_ALIAS": "別名", - "LABEL_DOMAIN": "域名" - }, - "POPUPS_DOMAIN": { - "TITLE_ADD_DOMAIN": "新增域名", - "TITLE_ADD_DOMAIN_WITH_NAME": "新增域名 \"%NAME%\"", - "TITLE_EDIT_DOMAIN": "編輯域名 \"%NAME%\"", - "LABEL_NAME": "名字", - "NAME_HELPER": "支援萬用字元", - "LABEL_SERVER": "伺服器", - "LABEL_PORT": "埠", - "LABEL_SECURE": "加密", - "TIMEOUT": "超時", - "DISABLE_CAPABILITIES": "禁用能力", - "LIMITS": "限制", - "FOLDERS": "資料夾", - "MESSAGES": "訊息", - "SECURE_OPTION_NONE": "無", - "LABEL_ALLOW_SIEVE_SCRIPTS": "可使用篩選指令碼", - "LABEL_USE_SHORT_LOGIN": "使用短使用者名稱登入", - "LABEL_LOWERCASE_LOGIN": "小寫登入 (大小寫敏感)", - "LABEL_USE_AUTH": "使用驗證", - "LABEL_SET_SENDER": "使用登入名作為發件人", - "LABEL_AUTH_PLAIN_LINE": "強制單一 AUTH PLAIN 命令登入", - "LABEL_USE_PHP_MAIL": "使用 php mail() 函式", - "BUTTON_WHITE_LIST": "白名單", - "NEW_DOMAIN_DESC": "此域名配置將允許您和%NAME%<\/strong>電子郵件地址一起工作.", - "WHITE_LIST_ALERT": "允許訪問的域使用者;使用空格分隔。\n", - "VALID_SPAM_HEADERS": "Valid SPAM headers", - "VALID_VIRUS_HEADERS": "Valid virus headers" - }, - "POPUPS_PLUGIN": { - "TITLE_PLUGIN": "外掛", - "DESC_NOTHING_TO_CONFIGURE": "無可用配置" - }, - "POPUPS_ASK": { - "BUTTON_YES": "是", - "BUTTON_NO": "否", - "DESC_WANT_CLOSE_THIS_WINDOW": "您確定要關閉此視窗嗎?" - }, - "POPUPS_LANGUAGES": { - "TITLE_LANGUAGES": "選擇您的語言" - }, - "HINTS": { - "BETA": "測試版本", - "WARNING": "警告" - }, - "ERRORS": { - "DOMAIN_ALREADY_EXISTS": "域名已經存在", - "UNKNOWN_ERROR": "未知錯誤" - }, - "NOTIFICATIONS": { - "RequestError": "請求失敗", - "RequestAborted": "請求中止", - "RequestTimeout": "請求超時", - "InvalidToken": "標記無效", - "AuthError": "驗證失敗", - "DomainAlreadyExists": "域名已經存在", - "CantSavePluginSettings": "無法儲存設定", - "CantInstallPackage": "安裝外掛包失敗", - "CantDeletePackage": "移除外掛包失敗", - "InvalidPluginPackage": "無效外掛包", - "UnsupportedPluginPackage": "不支援的外掛包", - "UnknownError": "未知錯誤" - } -} \ No newline at end of file +{ + "GLOBAL": { + "ADD": "新增", + "CLOSE": "關閉", + "SAVE": "儲存", + "TEST": "測試", + "UPDATE": "更新", + "USERNAME": "使用者名稱", + "PASSWORD": "密碼", + "CANCEL": "取消", + "SEARCH": "搜尋" + }, + "LOGIN": { + "LABEL_LOGIN": "登入", + "LABEL_TOTP": "TOTP 密碼", + "BUTTON_LOGIN": "登入到控制面板" + }, + "TOP_PANEL": { + "LABEL_ADMIN_PANEL": "控制面板" + }, + "SETTINGS_LABELS": { + "GENERAL": "常規", + "LOGIN": "登入", + "BRANDING": "Logo", + "CONTACTS": "聯絡人", + "DOMAINS": "域名", + "SECURITY": "安全", + "PACKAGES": "外掛包", + "CONFIG": "配置", + "ABOUT": "關於" + }, + "TAB_GENERAL": { + "LEGEND_INTERFACE": "介面", + "LABEL_LANGUAGE": "語言", + "LABEL_LANGUAGE_ADMIN": "語言(後臺)", + "LABEL_THEME": "主題", + "LABEL_ALLOW_LANGUAGES_ON_SETTINGS": "允許在設定介面中選擇語言", + "LABEL_ALLOW_THEMES_ON_SETTINGS": "允許在設定介面中選擇主題", + "LABEL_ALLOW_BACKGROUND_ON_SETTINGS": "允許在設定介面中選擇背景", + "LABEL_SHOW_THUMBNAILS": "顯示縮圖(附件)", + "LEGEND_MAIN": "主要設定", + "LABEL_ATTACHMENT_SIZE_LIMIT": "附件大小限制", + "LABEL_ALLOW_ADDITIONAL_ACCOUNTS": "允許額外的賬戶", + "LABEL_ALLOW_IDENTITIES": "允許多重身份", + "ALERT_DATA_ACCESS": "資料資料夾可被訪問。請配置您的 Web 伺服器以阻止從外部訪問資料資料夾。獲取更多幫助:", + "ALERT_WARNING": "警告!", + "HTML_ALERT_WEAK_PASSWORD": "您正在使用預設的管理員密碼\n
\n安全起見,請立即將密碼\n
更改<\/a><\/strong>\n為其他的字串。\n" + }, + "TAB_LOGIN": { + "LEGEND_LOGIN_SCREEN": "登入介面", + "LABEL_DEFAULT_DOMAIN": "預設域名", + "LABEL_DETERMINE_USER_DOMAIN": "嘗試判斷使用者使用的域名", + "LABEL_ALLOW_LANGUAGES_ON_LOGIN": "允許在登入介面選擇語言", + "LABEL_DETERMINE_USER_LANGUAGE": "嘗試判斷使用者使用語言" + }, + "TAB_BRANDING": { + "LEGEND_BRANDING": "Logo", + "LABEL_PAGE_TITLE": "頁面標題", + "LABEL_LOADING_DESCRIPTION": "載入介面文字", + "LABEL_FAVICON_URL": "網站圖示" + }, + "TAB_CONTACTS": { + "LEGEND_CONTACTS": "聯絡人", + "LEGEND_STORAGE": "儲存(PDO)", + "LABEL_ENABLE_CONTACTS": "啟用聯絡人", + "LABEL_ALLOW_SYNC": "允許同步聯絡人(依靠外部CardDAV伺服器)", + "LABEL_STORAGE_TYPE": "型別", + "LABEL_STORAGE_DSN": "Dsn", + "LABEL_STORAGE_USER": "使用者", + "LABEL_STORAGE_PASSWORD": "密碼", + "SQLITE_GLOBAL": "使用全域性資料庫替代各賬號的資料庫", + "ALERT_NOTICE": "提示!", + "SUGGESTIONS_LIMIT": "建議數量上限", + "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "如果有大量的活躍使用者,請不要選擇此資料庫型別。", + "HTML_ALERT_DOES_NOT_SUPPORTED": "您的系統不支援聯絡人\n
\n您需要在您的伺服器上安裝或啟用PDO (SQLite \/ MySQL \/ PostgreSQL)<\/strong> 元件。\n" + }, + "TAB_DOMAINS": { + "LEGEND_DOMAINS": "域名", + "BUTTON_ADD_DOMAIN": "新增域名", + "BUTTON_ADD_ALIAS": "新增別名", + "DELETE_ARE_YOU_SURE": "您確定嗎?", + "HTML_DOMAINS_HELPER": "允許訪問的域名列表。\n
\n點選域名進行設定。\n" + }, + "TAB_SECURITY": { + "LEGEND_SECURITY": "安全", + "LABEL_USE_IMAGE_PROXY": "對外部圖片使用本地代理", + "LABEL_AUTO_VERIFY": "自動驗證簽名", + "LABEL_SHOW_PHP_INFO": "顯示 PHP 資訊", + "LEGEND_ADMIN_PANEL_ACCESS_CREDENTIALS": "管理員面板訪問憑據", + "LABEL_CURRENT_PASSWORD": "當前密碼", + "LABEL_NEW_PASSWORD": "新密碼", + "LABEL_REPEAT_PASSWORD": "重複輸入新密碼", + "LABEL_REQUIRE_VERIFICATION": "要求驗證 SSL 證書", + "CRYPTO": "加密", + "LABEL_ALLOW_SELF_SIGNED": "允許自簽名證書" + }, + "TAB_PACKAGES": { + "LABEL_ENABLE_PLUGINS": "啟用外掛", + "LEGEND_AVAILABLE_FOR_UPDATE": "有更新可用", + "LEGEND_AVAILABLE_FOR_INSTALLATION": "可用於安裝", + "LEGEND_INSTALLED_PACKAGES": "已安裝的外掛包", + "ALERT_CANNOT_ACCESS_REPOSITORY": "目前無法訪問外掛倉庫" + }, + "TAB_ABOUT": { + "LEGEND_ABOUT": "關於", + "LABEL_TAG_HINT": "簡潔、現代化、快速的網頁郵件客戶端", + "LABEL_ALL_RIGHTS_RESERVED": "保留所有權利", + "HINT_READ_CHANGE_LOG": "請在更新前閱讀更新日誌", + "HINT_IS_UP_TO_DATE": "SnappyMail已是最新版本", + "HTML_NEW_VERSION": "有新的%VERSION%<\/b> 版本可用.", + "LABEL_UPDATING": "更新中", + "LABEL_CHECKING": "檢查更新", + "BUTTON_RELEASES": "發行版" + }, + "POPUPS_DOMAIN_ALIAS": { + "TITLE_ADD_DOMAIN_ALIAS": "新增別名", + "LABEL_ALIAS": "別名", + "LABEL_DOMAIN": "域名" + }, + "POPUPS_DOMAIN": { + "TITLE_ADD_DOMAIN": "新增域名", + "TITLE_ADD_DOMAIN_WITH_NAME": "新增域名 \"%NAME%\"", + "TITLE_EDIT_DOMAIN": "編輯域名 \"%NAME%\"", + "LABEL_NAME": "名字", + "NAME_HELPER": "支援萬用字元", + "LABEL_SERVER": "伺服器", + "LABEL_PORT": "埠", + "LABEL_SECURE": "加密", + "TIMEOUT": "超時", + "DISABLE_CAPABILITIES": "禁用能力", + "LIMITS": "限制", + "FOLDERS": "資料夾", + "MESSAGES": "訊息", + "SECURE_OPTION_NONE": "無", + "LABEL_ALLOW_SIEVE_SCRIPTS": "可使用篩選指令碼", + "LABEL_USE_SHORT_LOGIN": "使用短使用者名稱登入", + "LABEL_LOWERCASE_LOGIN": "小寫登入 (大小寫敏感)", + "LABEL_USE_AUTH": "使用驗證", + "LABEL_SET_SENDER": "使用登入名作為發件人", + "LABEL_AUTH_PLAIN_LINE": "強制單一 AUTH PLAIN 命令登入", + "LABEL_USE_PHP_MAIL": "使用 php mail() 函式", + "BUTTON_WHITE_LIST": "白名單", + "NEW_DOMAIN_DESC": "此域名配置將允許您和%NAME%<\/strong>電子郵件地址一起工作.", + "WHITE_LIST_ALERT": "允許訪問的域使用者;使用空格分隔。\n", + "VALID_SPAM_HEADERS": "Valid SPAM headers", + "VALID_VIRUS_HEADERS": "Valid virus headers" + }, + "POPUPS_PLUGIN": { + "TITLE_PLUGIN": "外掛", + "DESC_NOTHING_TO_CONFIGURE": "無可用配置" + }, + "POPUPS_ASK": { + "BUTTON_YES": "是", + "BUTTON_NO": "否", + "DESC_WANT_CLOSE_THIS_WINDOW": "您確定要關閉此視窗嗎?" + }, + "POPUPS_LANGUAGES": { + "TITLE_LANGUAGES": "選擇您的語言" + }, + "HINTS": { + "BETA": "測試版本", + "WARNING": "警告" + }, + "ERRORS": { + "DOMAIN_ALREADY_EXISTS": "域名已經存在", + "UNKNOWN_ERROR": "未知錯誤" + }, + "NOTIFICATIONS": { + "RequestError": "請求失敗", + "RequestAborted": "請求中止", + "RequestTimeout": "請求超時", + "InvalidToken": "標記無效", + "AuthError": "驗證失敗", + "DomainAlreadyExists": "域名已經存在", + "CantSavePluginSettings": "無法儲存設定", + "CantInstallPackage": "安裝外掛包失敗", + "CantDeletePackage": "移除外掛包失敗", + "InvalidPluginPackage": "無效外掛包", + "UnsupportedPluginPackage": "不支援的外掛包", + "UnknownError": "未知錯誤" + } +} From cb7dafba4d59b55ba82382e2fe1a9dc95947ec8f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 25 Aug 2024 17:35:14 +0200 Subject: [PATCH 04/26] Cleanup Cacher handling and added some documentation --- snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index d3f3914ea..bed876eb2 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -213,6 +213,13 @@ class Actions case 'suggestions': $mResult = []; break; +/* // See function Cacher + case 'cache': + $mResult = new \MailSo\Cache\Drivers\File( + \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache' + ); + break; +*/ } } @@ -460,6 +467,9 @@ class Actions return $this->oDomainProvider; } + /** + * bForceFile is only used by admin session token + */ public function Cacher(?Model\Account $oAccount = null, bool $bForceFile = false): \MailSo\Cache\CacheClient { $sKey = ''; @@ -481,7 +491,6 @@ class Actions \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache' ); } -// $sDriver = \strtoupper(\trim($this->oConfig->Get('cache', 'fast_cache_driver', 'files'))); $oDriver->setPrefix($sKey); $this->aCachers[$sIndexKey]->SetDriver($oDriver); From 34f730a196d3c571fbdfa011466ba16f230efc6d Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 25 Aug 2024 18:04:59 +0200 Subject: [PATCH 05/26] Added markedjs for #1604 --- .eslintrc.js | 6 +- vendors/marked/marked.js | 2483 ++++++++++++++++++++++++++++++++++ vendors/turndown/turndown.js | 1 - 3 files changed, 2488 insertions(+), 2 deletions(-) create mode 100644 vendors/marked/marked.js diff --git a/.eslintrc.js b/.eslintrc.js index 93b9fc12e..e22cb986d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -37,7 +37,11 @@ module.exports = { // Mailvelope 'mailvelope': "readonly", // Punycode - 'IDN': "readonly" + 'IDN': "readonly", + // Turndown + 'TurndownService': "readonly", + // Marked + 'marked': "readonly" }, // http://eslint.org/docs/rules/ rules: { diff --git a/vendors/marked/marked.js b/vendors/marked/marked.js new file mode 100644 index 000000000..46ed21302 --- /dev/null +++ b/vendors/marked/marked.js @@ -0,0 +1,2483 @@ +/*eslint max-len: "off"*/ +/*eslint no-control-regex: "off"*/ +/** + * marked v14.0.0 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + * + * modified for SnappyMail + */ + +const marked = (() => { + + /** + * Gets the original marked default options. + */ + const _getDefaults = () => ({ + async: false, + breaks: false, + extensions: null, + gfm: true, + hooks: null, + pedantic: false, + renderer: null, + silent: false, + tokenizer: null, + walkTokens: null, + }); + var exports_defaults = _getDefaults(); + + /** + * Helpers + */ + const escapeTest = /[&<>"']/; + const escapeReplace = new RegExp(escapeTest.source, 'g'); + const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; + const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); + const escapeReplacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + const getEscapeReplacement = (ch) => escapeReplacements[ch]; + const escape$1 = (html, encode) => { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } + else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); + } + } + return html; + }; + const caret = /(^|[^[])\^/g; + const edit = (regex, opt) => { + let source = typeof regex === 'string' ? regex : regex.source; + opt = opt || ''; + const obj = { + replace: (name, val) => { + let valSource = typeof val === 'string' ? val : val.source; + valSource = valSource.replace(caret, '$1'); + source = source.replace(name, valSource); + return obj; + }, + getRegex: () => { + return new RegExp(source, opt); + }, + }; + return obj; + }; + const cleanUrl = (href) => { + try { + href = encodeURI(href).replace(/%25/g, '%'); + } + catch { + return null; + } + return href; + }; + const noopTest = { exec: () => null }; + const splitCells = (tableRow, count) => { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(/\|/g, (match, offset, str) => { + let escaped = false; + let curr = offset; + while (--curr >= 0 && str[curr] === '\\') + escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } + else { + // add space before unescaped | + return ' |'; + } + }), cells = row.split(/ \|/); + let i = 0; + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { + cells.shift(); + } + if (cells.length > 0 && !cells[cells.length - 1].trim()) { + cells.pop(); + } + if (count) { + if (cells.length > count) { + cells.splice(count); + } + else { + while (cells.length < count) + cells.push(''); + } + } + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + return cells; + }; + /** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param str + * @param c + * @param invert Remove suffix of non-c chars instead. Default falsey. + */ + const rtrim = (str, c, invert) => { + const l = str.length; + if (l === 0) { + return ''; + } + // Length of suffix matching the invert condition. + let suffLen = 0; + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } + else if (currChar !== c && invert) { + suffLen++; + } + else { + break; + } + } + return str.slice(0, l - suffLen); + }; + const findClosingBracket = (str, b) => { + if (str.indexOf(b[1]) === -1) { + return -1; + } + let level = 0; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } + else if (str[i] === b[0]) { + level++; + } + else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; + }; + + const outputLink = (cap, link, raw, lexer) => { + const href = link.href; + const title = link.title ? escape$1(link.title) : null; + const text = cap[1].replace(/\\([[\]])/g, '$1'); + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text), + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text: escape$1(text), + }; + }; + const indentCodeCompensation = (raw, text) => { + const matchIndentToCode = raw.match(/^(\s+)(?:```)/); + if (matchIndentToCode === null) { + return text; + } + const indentToCode = matchIndentToCode[1]; + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(/^\s+/); + if (matchIndentInNode === null) { + return node; + } + const [indentInNode] = matchIndentInNode; + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + return node; + }) + .join('\n'); + }; + + /** + * Tokenizer + */ + class _Tokenizer { + constructor(options) { + this.options = options || exports_defaults; +// this.rules; // set by the lexer +// this.lexer; // set by the lexer + } + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0], + }; + } + } + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(/^ {1,4}/gm, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text, + }; + } + } + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || ''); + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2], + text, + }; + } + } + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + // remove trailing #s + if (/#$/.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } + else if (!trimmed || / $/.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text), + }; + } + } + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: rtrim(cap[0], '\n'), + }; + } + } + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + let lines = rtrim(cap[0], '\n').split('\n'); + let raw = ''; + let text = ''; + const tokens = []; + while (lines.length > 0) { + let inBlockquote = false; + const currentLines = []; + let i; + for (i = 0; i < lines.length; i++) { + // get lines up to a continuation + if (/^ {0,3}>/.test(lines[i])) { + currentLines.push(lines[i]); + inBlockquote = true; + } + else if (!inBlockquote) { + currentLines.push(lines[i]); + } + else { + break; + } + } + lines = lines.slice(i); + const currentRaw = currentLines.join('\n'); + const currentText = currentRaw + // precede setext continuation with 4 spaces so it isn't a setext + .replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g, '\n $1') + .replace(/^ {0,3}>[ \t]?/gm, ''); + raw = raw ? `${raw}\n${currentRaw}` : currentRaw; + text = text ? `${text}\n${currentText}` : currentText; + // parse blockquote lines as top level tokens + // merge paragraphs if this is a continuation + const top = this.lexer.state.top; + this.lexer.state.top = true; + this.lexer.blockTokens(currentText, tokens, true); + this.lexer.state.top = top; + // if there is no continuation then we are done + if (lines.length === 0) { + break; + } + const lastToken = tokens[tokens.length - 1]; + if (lastToken?.type === 'code') { + // blockquote continuation cannot be preceded by a code block + break; + } + else if (lastToken?.type === 'blockquote') { + // include continuation in nested blockquote + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.blockquote(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.text.length) + newToken.text; + break; + } + else if (lastToken?.type === 'list') { + // include continuation in nested list + const oldToken = lastToken; + const newText = oldToken.raw + '\n' + lines.join('\n'); + const newToken = this.list(newText); + tokens[tokens.length - 1] = newToken; + raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw; + text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw; + lines = newText.substring(tokens[tokens.length - 1].raw.length).split('\n'); + continue; + } + } + return { + type: 'blockquote', + raw, + tokens, + text, + }; + } + } + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let bull = cap[1].trim(); + const isordered = bull.length > 1; + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [], + }; + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + // Get next list item + const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`); + let endsWithBlankLine = false; + // Check if current bullet point can start a new List Item + while (src) { + let endEarly = false; + let raw = ''; + let itemContents = ''; + if (!(cap = itemRegex.exec(src))) { + break; + } + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + raw = cap[0]; + src = src.substring(raw.length); + let line = cap[2].split('\n', 1)[0].replace(/^\t+/, (t) => ' '.repeat(3 * t.length)); + let nextLine = src.split('\n', 1)[0]; + let blankLine = !line.trim(); + let indent = 0; + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimStart(); + } + else if (blankLine) { + indent = cap[1].length + 1; + } + else { + indent = cap[2].search(/[^ ]/); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + if (blankLine && /^ *$/.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + if (!endEarly) { + const nextBulletRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`); + const hrRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`); + const fencesBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`); //` + const headingBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`); + // Check if following lines should be included in List Item + while (src) { + const rawLine = src.split('\n', 1)[0]; + nextLine = rawLine; + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); + } + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + // Horizontal rule found + if (hrRegex.test(src)) { + break; + } + if (nextLine.search(/[^ ]/) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLine.slice(indent); + } + else { + // not enough indentation + if (blankLine) { + break; + } + // paragraph continuation unless last line was a different block level element + if (line.search(/[^ ]/) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + itemContents += '\n' + nextLine; + } + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLine.slice(indent); + } + } + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } + else if (/\n *\n *$/.test(raw)) { + endsWithBlankLine = true; + } + } + let istask = null; + let ischecked; + // Check for task list items + if (this.options.gfm) { + istask = /^\[[ xX]\] /.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(/^\[[ xX]\] +/, ''); + } + } + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents, + tokens: [], + }); + list.raw += raw; + } + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + list.items[list.items.length - 1].raw = list.items[list.items.length - 1].raw.trimEnd(); + list.items[list.items.length - 1].text = list.items[list.items.length - 1].text.trimEnd(); + list.raw = list.raw.trimEnd(); + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (let i = 0; i < list.items.length; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => /\n.*\n/.test(t.raw)); + list.loose = hasMultipleLineBreaks; + } + } + // Set all items to loose if list is loose + if (list.loose) { + for (let i = 0; i < list.items.length; i++) { + list.items[i].loose = true; + } + } + return list; + } + } + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0], + }; + return token; + } + } + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + const href = cap[2] ? cap[2].replace(/^<(.*)>$/, '$1').replace(this.rules.inline.anyPunctuation, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title, + }; + } + } + table(src) { + const cap = this.rules.block.table.exec(src); + if (!cap) { + return; + } + if (!/[:|]/.test(cap[2])) { + // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading + return; + } + const headers = splitCells(cap[1]); + const aligns = cap[2].replace(/^\||\| *$/g, '').split('|'); + const rows = cap[3] && cap[3].trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : []; + const item = { + type: 'table', + raw: cap[0], + header: [], + align: [], + rows: [], + }; + if (headers.length !== aligns.length) { + // header and align columns must be equal, rows can be different. + return; + } + for (const align of aligns) { + if (/^ *-+: *$/.test(align)) { + item.align.push('right'); + } + else if (/^ *:-+: *$/.test(align)) { + item.align.push('center'); + } + else if (/^ *:-+ *$/.test(align)) { + item.align.push('left'); + } + else { + item.align.push(null); + } + } + for (let i = 0; i < headers.length; i++) { + item.header.push({ + text: headers[i], + tokens: this.lexer.inline(headers[i]), + header: true, + align: item.align[i], + }); + } + for (const row of rows) { + item.rows.push(splitCells(row, item.header.length).map((cell, i) => { + return { + text: cell, + tokens: this.lexer.inline(cell), + header: false, + align: item.align[i], + }; + })); + } + return item; + } + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]), + }; + } + } + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text), + }; + } + } + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]), + }; + } + } + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: escape$1(cap[1]), + }; + } + } + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && /^
/i.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } + else if (this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + return { + type: 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: cap[0], + }; + } + } + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && /^$/.test(trimmedUrl))) { + return; + } + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } + else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + if (link) { + href = link[1]; + title = link[3]; + } + } + else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim(); + if (/^$/.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } + else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href, + title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title, + }, cap[0], this.lexer); + } + } + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + const linkString = (cap[2] || cap[1]).replace(/\s+/g, ' '); + const link = links[linkString.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text, + }; + } + return outputLink(cap, link, cap[0], this.lexer); + } + } + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrongLDelim.exec(src); + if (!match) + return; + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(/[\p{L}\p{N}]/u)) + return; + const nextChar = match[1] || match[2] || ''; + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below) + const lLength = [...match[0]].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + endReg.lastIndex = 0; + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) + continue; // skip single * in __abc*abc__ + rLength = [...rDelim].length; + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } + else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + delimTotal -= rLength; + if (delimTotal > 0) + continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + // char length can be >1 for unicode characters; + const lastCharLength = [...match[0]][0].length; + const raw = src.slice(0, lLength + match.index + lastCharLength + rLength); + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text), + }; + } + } + } + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(/\n/g, ' '); + const hasNonSpaceChars = /[^ ]/.test(text); + const hasSpaceCharsOnBothEnds = /^ /.test(text) && / $/.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + text = escape$1(text, true); + return { + type: 'codespan', + raw: cap[0], + text, + }; + } + } + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0], + }; + } + } + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]), + }; + } + } + autolink(src) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = escape$1(cap[1]); + href = 'mailto:' + text; + } + else { + text = escape$1(cap[1]); + href = text; + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + url(src) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = escape$1(cap[0]); + href = 'mailto:' + text; + } + else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? ''; + } while (prevCapZero !== cap[0]); + text = escape$1(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } + else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text, + }, + ], + }; + } + } + inlineText(src) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + let text; + if (this.lexer.state.inRawBlock) { + text = cap[0]; + } + else { + text = escape$1(cap[0]); + } + return { + type: 'text', + raw: cap[0], + text, + }; + } + } + } + + /** + * Block-Level Grammar + */ + const newline = /^(?: *(?:\n|$))+/; + const blockCode = /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/; + const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; + const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; + const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; + const bullet = /(?:[*+-]|\d{1,9}[.)])/; + const lheading = edit(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/) + .replace(/bull/g, bullet) // lists can interrupt + .replace(/blockCode/g, / {4}/) // indented code blocks can interrupt + .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt + .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt + .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt + .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt + .getRegex(); + const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/; + const blockText = /^[^\n]+/; + const _blockLabel = /(?!\s*\])(?:\\.|[^[\]\\])+/; + const def = edit(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/) + .replace('label', _blockLabel) + .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) + .getRegex(); + const list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/) + .replace(/bull/g, bullet) + .getRegex(); + const _tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + + '|tr|track|ul'; + const _comment = /|$))/; + const html = edit('^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag + + ')', 'i') + .replace('comment', _comment) + .replace('tag', _tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); + const paragraph = edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) + .replace('paragraph', paragraph) + .getRegex(); + /** + * Normal Block Grammar + */ + const blockNormal = { + blockquote, + code: blockCode, + def, + fences, + heading, + hr, + html, + lheading, + list, + newline, + paragraph, + table: noopTest, + text: blockText, + }; + /** + * GFM Block Grammar + */ + const gfmTable = edit('^ *([^\\n ].*)\\n' // Header + + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('blockquote', ' {0,3}>') + .replace('code', ' {4}[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + const blockGfm = { + ...blockNormal, + table: gfmTable, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') + .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs + .replace('table', gfmTable) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', _tag) // pars can be interrupted by type (6) html blocks + .getRegex(), + }; + /** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ + const blockPedantic = { + ...blockNormal, + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', _comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(_paragraph) + .replace('hr', hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', lheading) + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .replace('|tag', '') + .getRegex(), + }; + /** + * Inline-Level Grammar + */ + const escape = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/; + const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; + const br = /^( {2,}|\\)\n(?!\s*$)/; + const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ + const blockSkip = /\[[^[\]]*?\]\([^()]*?\)|`[^`]*?`|<[^<>]*?>/g; + const emStrongLDelim = edit(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, 'u') + .replace(/punct/g, _punctuation) + .getRegex(); + const emStrongRDelimAst = edit('^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + + '|[^*]+(?=[^*])' // Consume to delim + + '|(?!\\*)[punct](\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + + '|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)' // (2) a***#, a*** can only be a Right Delimiter + + '|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])' // (3) #***a, ***a can only be Left Delimiter + + '|[\\s](\\*+)(?!\\*)(?=[punct])' // (4) ***# can only be Left Delimiter + + '|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])' // (5) #***# can be either Left or Right Delimiter + + '|[^punct\\s](\\*+)(?=[^punct\\s])', 'gu') // (6) a***a can be either Left or Right Delimiter + .replace(/punct/g, _punctuation) + .getRegex(); + // (6) Not allowed for _ + const emStrongRDelimUnd = edit('^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + + '|[^_]+(?=[^_])' // Consume to delim + + '|(?!_)[punct](_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + + '|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)' // (2) a___#, a___ can only be a Right Delimiter + + '|(?!_)[punct\\s](_+)(?=[^punct\\s])' // (3) #___a, ___a can only be Left Delimiter + + '|[\\s](_+)(?!_)(?=[punct])' // (4) ___# can only be Left Delimiter + + '|(?!_)[punct](_+)(?!_)(?=[punct])', 'gu') // (5) #___# can be either Left or Right Delimiter + .replace(/punct/g, _punctuation) + .getRegex(); + const anyPunctuation = edit(/\\([punct])/, 'gu') + .replace(/punct/g, _punctuation) + .getRegex(); + const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) + .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) + .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) + .getRegex(); + const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); + const tag = edit('^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^') // CDATA section + .replace('comment', _inlineComment) + .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) + .getRegex(); + const _inlineLabel = /(?:\[(?:\\.|[^[\]\\])*\]|\\.|`[^`]*`|[^[\]\\`])*?/; + const link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/) + .replace('label', _inlineLabel) + .replace('href', /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/) + .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) + .getRegex(); + const reflink = edit(/^!?\[(label)\]\[(ref)\]/) + .replace('label', _inlineLabel) + .replace('ref', _blockLabel) + .getRegex(); + const nolink = edit(/^!?\[(ref)\](?:\[\])?/) + .replace('ref', _blockLabel) + .getRegex(); + const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') + .replace('reflink', reflink) + .replace('nolink', nolink) + .getRegex(); + /** + * Normal Inline Grammar + */ + const inlineNormal = { + _backpedal: noopTest, // only used for GFM url + anyPunctuation, + autolink, + blockSkip, + br, + code: inlineCode, + del: noopTest, + emStrongLDelim, + emStrongRDelimAst, + emStrongRDelimUnd, + escape, + link, + nolink, + punctuation, + reflink, + reflinkSearch, + tag, + text: inlineText, + url: noopTest, + }; + /** + * Pedantic Inline Grammar + */ + const inlinePedantic = { + ...inlineNormal, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', _inlineLabel) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', _inlineLabel) + .getRegex(), + }; + /** + * GFM Inline Grammar + */ + const inlineGfm = { + ...inlineNormal, + escape: edit(escape).replace('])', '~|])').getRegex(), + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9-]+\.?)+[^\s<]*|^email/, 'i') + .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+/=?_`{|}~-]+@)|[\s\S]*?(?:(?=[\\ { + return leading + ' '.repeat(tabs.length); + }); + } + let token; + let lastToken; + let cutSrc; + while (src) { + if (this.options.extensions + && this.options.extensions.block + && this.options.extensions.block.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + if (token.raw.length === 1 && tokens.length > 0) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unnecessary paragraph tags + tokens[tokens.length - 1].raw += '\n'; + } + else { + tokens.push(token); + } + continue; + } + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + // An indented code block cannot interrupt a paragraph. + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title, + }; + } + continue; + } + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + lastToken = tokens[tokens.length - 1]; + if (lastParagraphClipped && lastToken?.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + lastParagraphClipped = (cutSrc.length !== src.length); + src = src.substring(token.raw.length); + continue; + } + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + this.state.top = true; + return tokens; + } + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + let token, lastToken, cutSrc; + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match; + let keepPrevChar, prevChar; + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + // extensions + if (this.options.extensions + && this.options.extensions.inline + && this.options.extensions.inline.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // autolink + if (token = this.tokenizer.autolink(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach((getStartIndex) => { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } + else { + tokens.push(token); + } + continue; + } + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } + else { + throw new Error(errMsg); + } + } + } + return tokens; + } + } + + /** + * Renderer + */ + class _Renderer { + constructor(options) { + this.options = options || exports_defaults; +// this.parser = null; // set by the parser + } + space() { + return ''; + } + code({ text, lang, escaped }) { + const langString = (lang || '').match(/^\S*/)?.[0]; + const code = text.replace(/\n$/, '') + '\n'; + if (!langString) { + return '
'
+					+ (escaped ? code : escape$1(code, true))
+					+ '
\n'; + } + return '
'
+				+ (escaped ? code : escape$1(code, true))
+				+ '
\n'; + } + blockquote({ tokens }) { + const body = this.parser.parse(tokens); + return `
\n${body}
\n`; + } + html({ text }) { + return text; + } + heading({ tokens, depth }) { + return `${this.parser.parseInline(tokens)}\n`; + } + hr() { + return '
\n'; + } + list(token) { + const ordered = token.ordered; + const start = token.start; + let body = ''; + for (let j = 0; j < token.items.length; j++) { + const item = token.items[j]; + body += this.listitem(item); + } + const type = ordered ? 'ol' : 'ul'; + const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startAttr + '>\n' + body + '\n'; + } + listitem(item) { + let itemBody = ''; + if (item.task) { + const checkbox = this.checkbox({ checked: !!item.checked }); + if (item.loose) { + if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; + } + } + else { + item.tokens.unshift({ + type: 'text', + raw: checkbox + ' ', + text: checkbox + ' ', + }); + } + } + else { + itemBody += checkbox + ' '; + } + } + itemBody += this.parser.parse(item.tokens, !!item.loose); + return `
  • ${itemBody}
  • \n`; + } + checkbox({ checked }) { + return ''; + } + paragraph({ tokens }) { + return `

    ${this.parser.parseInline(tokens)}

    \n`; + } + table(token) { + let header = ''; + // header + let cell = ''; + for (let j = 0; j < token.header.length; j++) { + cell += this.tablecell(token.header[j]); + } + header += this.tablerow({ text: cell }); + let body = ''; + for (let j = 0; j < token.rows.length; j++) { + const row = token.rows[j]; + cell = ''; + for (let k = 0; k < row.length; k++) { + cell += this.tablecell(row[k]); + } + body += this.tablerow({ text: cell }); + } + if (body) + body = `${body}`; + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + tablerow({ text }) { + return `\n${text}\n`; + } + tablecell(token) { + const content = this.parser.parseInline(token.tokens); + const type = token.header ? 'th' : 'td'; + const tag = token.align + ? `<${type} align="${token.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + /** + * span level renderer + */ + strong({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + em({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + codespan({ text }) { + return `${text}`; + } + br() { + return '
    '; + } + del({ tokens }) { + return `${this.parser.parseInline(tokens)}`; + } + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens); + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = '
    '; + return out; + } + image({ href, title, text }) { + const cleanHref = cleanUrl(href); + if (cleanHref === null) { + return text; + } + href = cleanHref; + let out = `${text} { + const tokens = genericToken[childTokens].flat(Infinity); + values = values.concat(this.walkTokens(tokens, callback)); + }); + } + else if (genericToken.tokens) { + values = values.concat(this.walkTokens(genericToken.tokens, callback)); + } + } + } + } + return values; + } + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if ('renderer' in ext) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function (...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } + else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if ('tokenizer' in ext) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + const extLevel = extensions[ext.level]; + if (extLevel) { + extLevel.unshift(ext.tokenizer); + } + else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } + else { + extensions.startBlock = [ext.start]; + } + } + else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } + else { + extensions.startInline = [ext.start]; + } + } + } + } + if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new _Renderer(this.defaults); + for (const prop in pack.renderer) { + if (!(prop in renderer)) { + throw new Error(`renderer '${prop}' does not exist`); + } + if (['options', 'parser'].includes(prop)) { + // ignore options property + continue; + } + const rendererProp = prop; + const rendererFunc = pack.renderer[rendererProp]; + const prevRenderer = renderer[rendererProp]; + // Replace renderer with func to run extension, but fall back if false + renderer[rendererProp] = (...args) => { + let ret = rendererFunc.apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret || ''; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + if (!(prop in tokenizer)) { + throw new Error(`tokenizer '${prop}' does not exist`); + } + if (['options', 'rules', 'lexer'].includes(prop)) { + // ignore options, rules, and lexer properties + continue; + } + const tokenizerProp = prop; + const tokenizerFunc = pack.tokenizer[tokenizerProp]; + const prevTokenizer = tokenizer[tokenizerProp]; + // Replace tokenizer with func to run extension, but fall back if false + // @ts-expect-error cannot type tokenizer function dynamically + tokenizer[tokenizerProp] = (...args) => { + let ret = tokenizerFunc.apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new _Hooks(); + for (const prop in pack.hooks) { + if (!(prop in hooks)) { + throw new Error(`hook '${prop}' does not exist`); + } + if (prop === 'options') { + // ignore options property + continue; + } + const hooksProp = prop; + const hooksFunc = pack.hooks[hooksProp]; + const prevHook = hooks[hooksProp]; + if (_Hooks.passThroughHooks.has(prop)) { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(hooksFunc.call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + const ret = hooksFunc.call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } + else { + // @ts-expect-error cannot type hook function dynamically + hooks[hooksProp] = (...args) => { + let ret = hooksFunc.apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + const packWalktokens = pack.walkTokens; + opts.walkTokens = function (token) { + let values = []; + values.push(packWalktokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + this.defaults = { ...this.defaults, ...opts }; + }); + return this; + } + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + lexer(src, options) { + return _Lexer.lex(src, options ?? this.defaults); + } + parser(tokens, options) { + return _Parser.parse(tokens, options ?? this.defaults); + } + parseMarkdown(lexer, parser) { + // eslint-disable-next-line + const parse = (src, options) => { + const origOpt = { ...options }; + const opt = { ...this.defaults, ...origOpt }; + const throwError = this.onError(!!opt.silent, !!opt.async); + // throw error if an extension set async to true but parse was called with async: false + if (this.defaults.async === true && origOpt.async === false) { + return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.')); + } + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + if (opt.hooks) { + opt.hooks.options = opt; + } + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.hooks ? opt.hooks.processAllTokens(tokens) : tokens) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + let tokens = lexer(src, opt); + if (opt.hooks) { + tokens = opt.hooks.processAllTokens(tokens); + } + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } + catch (e) { + return throwError(e); + } + }; + return parse; + } + onError(silent, async) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if (silent) { + const msg = '

    An error occurred:

    '
    +						+ escape$1(e.message + '', true)
    +						+ '
    '; + if (async) { + return Promise.resolve(msg); + } + return msg; + } + if (async) { + return Promise.reject(e); + } + throw e; + }; + } + } + + const markedInstance = new Marked(); +/* + function changeDefaults(newDefaults) { + exports_defaults = newDefaults; + } + + function marked(src, opt) { + return markedInstance.parse(src, opt); + } + /** + * Sets the default options. + * + * @param options Hash of options + * / + marked.options = + marked.setOptions = (options) => { + markedInstance.setOptions(options); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }; + /** + * Gets the original marked default options. + * / + marked.defaults = exports_defaults; + /** + * Run callback for every token + * / + marked.walkTokens = (tokens, callback) => markedInstance.walkTokens(tokens, callback); + /** + * Expose + * / + marked.Parser = _Parser; + marked.parser = _Parser.parse; + marked.Renderer = _Renderer; + marked.TextRenderer = _TextRenderer; + marked.Lexer = _Lexer; + marked.lexer = _Lexer.lex; + marked.Tokenizer = _Tokenizer; + marked.Hooks = _Hooks; + marked.parse = marked; +*/ + + return { +/* + Hooks: _Hooks, + Lexer: _Lexer, + Marked: Marked, + Parser: _Parser, + Renderer: _Renderer, + TextRenderer: _TextRenderer, + Tokenizer: _Tokenizer, + getDefaults: _getDefaults, + lexer: _Lexer.lex, + marked: marked, + options: marked.options, +*/ + parse: (src, opt) => markedInstance.parse(src, opt), +/* + /** + * Compiles markdown to HTML without enclosing `p` tag. + * + * @param src String of markdown source to be compiled + * @param options Hash of options + * @return String of compiled HTML + * / + parseInline: markedInstance.parseInline, + parser: _Parser.parse, + setOptions: marked.setOptions, + /** + * Use Extension + * / + use: (...args) => { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; + }, + walkTokens: marked.walkTokens +*/ + }; +})(); diff --git a/vendors/turndown/turndown.js b/vendors/turndown/turndown.js index 7e5a535a4..bc5acf95c 100644 --- a/vendors/turndown/turndown.js +++ b/vendors/turndown/turndown.js @@ -4,7 +4,6 @@ */ const TurndownService = (() => { - 'use strict'; const repeat = (character, count) => Array(count + 1).join(character), From a442322729f703b9228c65c14fe997bfb725e027 Mon Sep 17 00:00:00 2001 From: hguilbert <51283484+hguilbert@users.noreply.github.com> Date: Mon, 26 Aug 2024 06:14:29 +0200 Subject: [PATCH 06/26] Update user.json --- .../v/0.0.0/app/localization/fr/user.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/snappymail/v/0.0.0/app/localization/fr/user.json b/snappymail/v/0.0.0/app/localization/fr/user.json index 060808481..0f594a582 100644 --- a/snappymail/v/0.0.0/app/localization/fr/user.json +++ b/snappymail/v/0.0.0/app/localization/fr/user.json @@ -89,18 +89,18 @@ "ARCHIVE_NAME": "Archive" }, "IMAP": { - "ACL_RIGHTS": "Rights", - "ACL_A": "Administer", - "ACL_E": "Expunge", - "ACL_I": "Insert messages", - "ACL_K": "Create sub-folders", - "ACL_L": "Show messages", - "ACL_P": "Send mail", - "ACL_R": "Select folder", - "ACL_S": "\\SEEN tag", - "ACL_T": "\\DELETED tag", - "ACL_W": "Other tags", - "ACL_X": "Delete\/Rename folder" + "ACL_RIGHTS": "Droits", + "ACL_A": "Administrer", + "ACL_E": "Supprimer", + "ACL_I": "Insérer des messages", + "ACL_K": "Créer des sous-dossiers", + "ACL_L": "Afficher les messages", + "ACL_P": "Envoyer un courrier", + "ACL_R": "Sélectionner un dossier", + "ACL_S": "\\VUE balise", + "ACL_T": "\\SUPPRIMÉE balise", + "ACL_W": "Autres balises", + "ACL_X": "Supprimer\/Renommer le dossier" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Recharger la liste des messages", From daf02e53f33414a7ae51fc04a9426b79ac171890 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 26 Aug 2024 11:20:07 +0200 Subject: [PATCH 07/26] Resolve #1706 --- snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php | 1 + 1 file changed, 1 insertion(+) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php index 7cf2a82d7..6b2eb99aa 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -176,6 +176,7 @@ trait UserAuth $this->Plugins()->RunHook('login.success', array($oAccount)); $this->SetAuthToken($oAccount); + $this->SetAdditionalAuthToken(null); } return $oAccount; From 59652d2d61bec2583e27ec51f7b534f55c0d43fb Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 26 Aug 2024 13:22:41 +0200 Subject: [PATCH 08/26] Mark images with width=1 as tracking pixel --- dev/Common/Html.js | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 88dcde274..0557204dd 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -469,6 +469,7 @@ export const else if (detectHiddenImages && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in' || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in' + || (oStyle.width && 2 > pInt(oStyle.width)) || [ 'email.microsoftemail.com/open', 'github.com/notifications/beacon/', From 547953eb313b798f7712bee58c59782fe6337e98 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 26 Aug 2024 20:45:32 +0200 Subject: [PATCH 09/26] Resolve #1718 --- plugins/avatars/avatars.js | 4 ++++ plugins/avatars/index.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index 8aef7fa69..464c4f1cc 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -186,6 +186,10 @@ if (msg?.from?.[0]) { let url = getAvatar(msg), fn = url=>{element.src = url}; + element.onerror = ()=>{ + element.onerror = null; + setIdenticon(msg.from[0], fn); + }; if (url) { fn(url); } else if (msg.avatar?.startsWith('data:')) { diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index a1a5b921f..4baae52ce 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -10,8 +10,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'Avatars', AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', - VERSION = '1.19', - RELEASE = '2024-07-08', + VERSION = '1.20', + RELEASE = '2024-08-26', REQUIRED = '2.33.0', CATEGORY = 'Contacts', LICENSE = 'MIT', From 780e1f9b9284609406d9f7c023e08a063a54e371 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 11:49:11 +0200 Subject: [PATCH 10/26] Make sure only scalar values are allowed in $_ENV #1719 --- snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php | 1 + 1 file changed, 1 insertion(+) diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php index 7d56a35fa..2815c8010 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/pgp.php @@ -867,6 +867,7 @@ class PGP extends Base implements \SnappyMail\PGP\PGPInterface // Don't localize GnuPG results. $env = $_ENV; $env['LC_ALL'] = 'C'; + $env = \array_filter($env, fn($var) => \is_scalar($var)); $proc_pipes = []; From 9e2b9a9fcd77edf7a149a37f5d7d22294ef4992f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 12:12:33 +0200 Subject: [PATCH 11/26] Show GnuPG version for #1560 --- dev/Settings/Admin/Security.js | 2 ++ snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php | 3 +++ .../0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dev/Settings/Admin/Security.js b/dev/Settings/Admin/Security.js index 8bcac6e48..c9e9d8276 100644 --- a/dev/Settings/Admin/Security.js +++ b/dev/Settings/Admin/Security.js @@ -32,6 +32,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings { capaOpenPGP: SettingsCapa('OpenPGP') }); + this.gnuPGversion = 'GnuPG v' + SettingsGet('gnupg'); + const reset = () => { this.saveError(false); this.saveSuccess(false); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php index 6d3fac5aa..54a550d35 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php @@ -456,6 +456,9 @@ class ActionsAdmin extends Actions $aResult['Admin']['language'] = $oActions->ValidateLanguage($oConfig->Get('admin_panel', 'language', 'en'), '', true); $aResult['Admin']['languages'] = \SnappyMail\L10n::getLanguages(true); $aResult['Admin']['clientLanguage'] = $oActions->ValidateLanguage($oActions->detectClientLanguage(true), '', true, true); + + $gnupg = \SnappyMail\PGP\GnuPG::getInstance(''); + $aResult['gnupg'] = $gnupg ? $gnupg->getEngineInfo()['version'] : null; } else { $passfile = APP_PRIVATE_DATA.'admin_password.txt'; $sPassword = $oConfig->Get('security', 'admin_password', ''); diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html index e29fcd06c..d96bf34bf 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html @@ -17,7 +17,7 @@
    From 3306fadc472368685f44d2262cc78eb8a3633194 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 13:19:30 +0200 Subject: [PATCH 12/26] MS autodiscover priorities DNS over subdomain --- .../RainLoop/Providers/Domain/Autoconfig.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php index 75d94a651..b104276d8 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php @@ -168,16 +168,6 @@ abstract class Autoconfig */ private static function autodiscover(string $domain) : ?array { - foreach ([ - "https://{$domain}", - "https://autodiscover.{$domain}", - "http://autodiscover.{$domain}" - ] as $host) { - $result = static::autodiscover_resolve($host, $domain); - if ($result) { - return $result; - } - } foreach (\SnappyMail\DNS::SRV("_autodiscover._tcp.{$domain}") as $record) { if (443 == $record['port']) { $result = static::autodiscover_resolve("https://{$record['target']}", $domain); @@ -190,6 +180,16 @@ abstract class Autoconfig return $result; } } + foreach ([ + "https://{$domain}", + "https://autodiscover.{$domain}", + "http://autodiscover.{$domain}" + ] as $host) { + $result = static::autodiscover_resolve($host, $domain); + if ($result) { + return $result; + } + } return null; } From 9e3d6c6e239599497ec3e2d1e9719e5323822614 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 13:38:42 +0200 Subject: [PATCH 13/26] Show warning in Admin -> About when PHP runs in 32bit --- dev/Settings/Admin/About.js | 2 ++ .../app/templates/Views/Admin/AdminSettingsAbout.html | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dev/Settings/Admin/About.js b/dev/Settings/Admin/About.js index ef381683e..456fa250d 100644 --- a/dev/Settings/Admin/About.js +++ b/dev/Settings/Admin/About.js @@ -16,6 +16,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ { coreWarning: false, coreVersion: '', coreVersionCompare: -2, + php64: true, load1: 0, load5: 0, load15: 0, @@ -69,6 +70,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ { this.coreWarning(!!data.core.warning); this.coreVersion(data.core.version || ''); this.coreVersionCompare(data.core.versionCompare); + this.php64(data.php[1].loaded); } else { this.coreReal(false); this.coreWarning(false); diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html index 8b5a03115..b83ea9345 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html @@ -75,7 +75,12 @@ -

    PHP optional extensions

    +

    PHP

    +
    + From 1ac4f05d912ad8d4c700b8f1c4bee87fec2af8c8 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 15:27:44 +0200 Subject: [PATCH 14/26] Sabre/Xml v4.0.5 --- .../libraries/Sabre/Xml/Deserializer/functions.php | 7 +++++-- .../0.0.0/app/libraries/Sabre/Xml/LibXMLException.php | 2 +- snappymail/v/0.0.0/app/libraries/Sabre/Xml/Reader.php | 4 ++-- .../v/0.0.0/app/libraries/Sabre/Xml/Service.php | 11 +++++------ .../v/0.0.0/app/libraries/Sabre/Xml/Version.php | 2 +- snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php | 2 -- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php index 5531f7621..949aa720e 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Deserializer/functions.php @@ -57,7 +57,7 @@ use Sabre\Xml\Reader; * * @phpstan-return array */ -function keyValue(Reader $reader, string $namespace = null): array +function keyValue(Reader $reader, ?string $namespace = null): array { // If there's no children, we don't do anything. if ($reader->isEmptyElement) { @@ -148,7 +148,7 @@ function keyValue(Reader $reader, string $namespace = null): array * * @phpstan-return list */ -function enum(Reader $reader, string $namespace = null): array +function enum(Reader $reader, ?string $namespace = null): array { // If there's no children, we don't do anything. if ($reader->isEmptyElement) { @@ -223,6 +223,9 @@ function valueObject(Reader $reader, string $className, string $namespace): obje // Ignore property $reader->next(); } + } elseif (Reader::ELEMENT === $reader->nodeType) { + // Skipping element from different namespace + $reader->next(); } else { if (Reader::END_ELEMENT !== $reader->nodeType && !$reader->read()) { break; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php index 9b5332b3b..5c642a3bf 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/LibXMLException.php @@ -31,7 +31,7 @@ class LibXMLException extends ParseException * * @param \LibXMLError[] $errors */ - public function __construct(array $errors, int $code = 0, \Throwable $previousException = null) + public function __construct(array $errors, int $code = 0, ?\Throwable $previousException = null) { $this->errors = $errors; parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException); diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Reader.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Reader.php index 53d64d54d..a85bd058c 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Reader.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Reader.php @@ -107,7 +107,7 @@ class Reader extends \XMLReader * * @return array> */ - public function parseGetElements(array $elementMap = null): array + public function parseGetElements(?array $elementMap = null): array { $result = $this->parseInnerTree($elementMap); if (!is_array($result)) { @@ -132,7 +132,7 @@ class Reader extends \XMLReader * * @return array>|string|null */ - public function parseInnerTree(array $elementMap = null) + public function parseInnerTree(?array $elementMap = null) { $text = null; $elements = []; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php index cb9a5175b..ad18b6f4e 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Service.php @@ -107,7 +107,7 @@ class Service * * @throws ParseException */ - public function parse($input, string $contextUri = null, string &$rootElementName = null) + public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) { if (is_resource($input)) { // Unfortunately the XMLReader doesn't support streams. When it @@ -151,7 +151,7 @@ class Service * * @throws ParseException */ - public function expect($rootElementName, $input, string $contextUri = null) + public function expect($rootElementName, $input, ?string $contextUri = null) { if (is_resource($input)) { // Unfortunately the XMLReader doesn't support streams. When it @@ -200,7 +200,7 @@ class Service * * @param string|array|object|XmlSerializable $value */ - public function write(string $rootElementName, $value, string $contextUri = null): string + public function write(string $rootElementName, $value, ?string $contextUri = null): string { $w = $this->getWriter(); $w->openMemory(); @@ -241,7 +241,6 @@ class Service public function mapValueObject(string $elementName, string $className): void { list($namespace) = self::parseClarkNotation($elementName); - $namespace = $namespace ?? ''; require_once __DIR__ . '/Deserializer/functions.php'; $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) { @@ -265,7 +264,7 @@ class Service * * @throws \InvalidArgumentException */ - public function writeValueObject(object $object, string $contextUri = null): string + public function writeValueObject(object $object, ?string $contextUri = null): string { if (!isset($this->valueObjectMap[get_class($object)])) { throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.'); @@ -284,7 +283,7 @@ class Service * * If the string was invalid, it will throw an InvalidArgumentException. * - * @return array{string|null, string} + * @return array{string, string} * * @throws \InvalidArgumentException */ diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php index 1b3b4f9b3..f38e87c27 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Version.php @@ -16,5 +16,5 @@ class Version /** * Full version number. */ - public const VERSION = '4.0.4'; + public const VERSION = '4.0.5'; } diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php index e7d7eb941..77bdae02c 100644 --- a/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php +++ b/snappymail/v/0.0.0/app/libraries/Sabre/Xml/Writer.php @@ -125,7 +125,6 @@ class Writer extends \XMLWriter if ('{' === $name[0]) { list($namespace, $localName) = Service::parseClarkNotation($name); - $namespace = $namespace ?? ''; if (array_key_exists($namespace, $this->namespaceMap)) { $result = $this->startElementNS( @@ -240,7 +239,6 @@ class Writer extends \XMLWriter $localName ) = Service::parseClarkNotation($name); - $namespace = $namespace ?? ''; if (array_key_exists($namespace, $this->namespaceMap)) { // It's an attribute with a namespace we know return $this->writeAttribute( From 5e4520e10c2323e187e61484e31e170205b416db Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 16:20:39 +0200 Subject: [PATCH 15/26] Force Nextcloud personal language by default #1428 --- .../snappymail/lib/Controller/FetchController.php | 11 +++++++---- .../snappymail/lib/Migration/InstallStep.php | 2 ++ .../snappymail/lib/Settings/AdminSettings.php | 1 + .../nextcloud/snappymail/templates/admin-local.php | 7 +++++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/integrations/nextcloud/snappymail/lib/Controller/FetchController.php b/integrations/nextcloud/snappymail/lib/Controller/FetchController.php index 2b33ad945..6ba8a7761 100644 --- a/integrations/nextcloud/snappymail/lib/Controller/FetchController.php +++ b/integrations/nextcloud/snappymail/lib/Controller/FetchController.php @@ -61,11 +61,15 @@ class FetchController extends Controller { ]); } + SnappyMailHelper::loadApp(); + + $oConfig = \RainLoop\Api::Config(); if (!empty($_POST['snappymail-app_path'])) { - $oConfig = \RainLoop\Api::Config(); $oConfig->Set('webmail', 'app_path', $_POST['snappymail-app_path']); - $oConfig->Save(); } + $oConfig->Set('webmail', 'allow_languages_on_settings', empty($_POST['snappymail-nc-lang'])); + $oConfig->Set('login', 'allow_languages_on_login', empty($_POST['snappymail-nc-lang'])); + $oConfig->Save(); if (!empty($_POST['import-rainloop'])) { return new JSONResponse([ @@ -74,7 +78,6 @@ class FetchController extends Controller { ]); } - SnappyMailHelper::loadApp(); $debug = !empty($_POST['snappymail-debug']); $oConfig = \RainLoop\Api::Config(); if ($debug != $oConfig->Get('debug', 'enable', false)) { @@ -133,7 +136,7 @@ class FetchController extends Controller { // TODO: Handle both exceptions separately? SnappyMailHelper::loadApp(); \RainLoop\Api::Actions()->DoLogout(); - + return new JSONResponse([ 'status' => 'error', 'Message' => $e->getMessage() diff --git a/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php b/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php index cca015aa9..966ef2352 100644 --- a/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php +++ b/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php @@ -54,6 +54,8 @@ class InstallStep implements IRepairStep if (!$oConfig->Get('webmail', 'app_path')) { $output->info('Set config [webmail]app_path'); $oConfig->Set('webmail', 'app_path', \OC::$server->getAppManager()->getAppWebPath('snappymail') . '/app/'); + $oConfig->Set('webmail', 'allow_languages_on_settings', false); + $oConfig->Set('login', 'allow_languages_on_login', false); $bSave = true; } diff --git a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php index 82264bfba..7a69cb96f 100644 --- a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php +++ b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php @@ -71,6 +71,7 @@ class AdminSettings implements ISettings $oConfig->Save(); } $parameters['snappymail-app_path'] = $oConfig->Get('webmail', 'app_path', false); + $parameters['snappymail-nc-lang'] = !$oConfig->Get('webmail', 'allow_languages_on_settings', true); \OCP\Util::addScript('snappymail', 'snappymail'); return new TemplateResponse('snappymail', 'admin-local', $parameters); diff --git a/integrations/nextcloud/snappymail/templates/admin-local.php b/integrations/nextcloud/snappymail/templates/admin-local.php index abbc7f7d0..5270f3e50 100644 --- a/integrations/nextcloud/snappymail/templates/admin-local.php +++ b/integrations/nextcloud/snappymail/templates/admin-local.php @@ -71,6 +71,13 @@
    +

    + > + +

    +

    SnappyMail Webmail - 2.37.2 + 2.37.3 agpl SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli getAvatar($sEmail, !empty($aBimi[0]), $sBimiSelector))) { + \header("Cache-Control: max-age={$maxAge}, private"); + \header('Expires: '.\gmdate('D, j M Y H:i:s', $maxAge + \time()).' UTC'); \header('Content-Type: '.$aResult[0]); echo $aResult[1]; } else { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php index 4ad8d20d0..da6b9e199 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -184,6 +184,7 @@ trait Accounts $bReload = false; $oAccount = $this->getAccountFromToken(); if ($oAccount instanceof AdditionalAccount && $oAccount->Email() === $sEmailToDelete) { +// $this->SetAdditionalAuthToken(null); \SnappyMail\Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); $bReload = true; } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php index 74c474a7d..d94de5454 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/base.php @@ -76,12 +76,16 @@ abstract class Base function __destruct() { $this->proc_close(); - $gpgconf = static::findBinary('gpgconf'); if ($gpgconf) { + $cmd = $gpgconf . ' --kill gpg-agent'; + // https://github.com/the-djmaze/snappymail/issues/1560#issuecomment-2144817883 +// if (\version_compare($this->version, '2.4.0', '<')) { + $cmd .= ' ' . \escapeshellarg($this->options['homedir']); +// } $env = ['GNUPGHOME' => $this->options['homedir']]; $pipes = []; - if ($process = \proc_open($gpgconf . ' --kill gpg-agent --homedir ' . \escapeshellarg($this->options['homedir']), [], $pipes, null, $env)) { + if ($process = \proc_open($cmd, [], $pipes, null, $env)) { \proc_close($process); } } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php index aaa1c839e..203a180b0 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/gpg/smime.php @@ -58,20 +58,6 @@ class SMIME extends Base } } - function __destruct() - { - $this->proc_close(); - - $gpgconf = static::findBinary('gpgconf'); - if ($gpgconf) { - $env = ['GNUPGHOME' => $this->options['homedir']]; - $pipes = []; - if ($process = \proc_open($gpgconf . ' --kill gpg-agent --homedir ' . \escapeshellarg($this->options['homedir']), [], $pipes, null, $env)) { - \proc_close($process); - } - } - } - public static function isSupported() : bool { return parent::isSupported() && static::findBinary('gpgsm'); diff --git a/vendors/knockout/src/utils.js b/vendors/knockout/src/utils.js index e47cbabf9..a2129c2e3 100644 --- a/vendors/knockout/src/utils.js +++ b/vendors/knockout/src/utils.js @@ -25,6 +25,7 @@ ko.utils = { : node => node.cloneNode(true)), setDomNodeChildren: (domNode, childNodes) => { +// domNode.replaceChildren(...childNodes); ko.utils.emptyDomNode(domNode); childNodes && domNode.append(...childNodes); }, From 1c1682a192d594e72a2a1ec341659cdba19595ca Mon Sep 17 00:00:00 2001 From: ner0 <2269603+ner00@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:23:45 +0100 Subject: [PATCH 17/26] Update Portuguese --- .../v/0.0.0/app/localization/pt/user.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/snappymail/v/0.0.0/app/localization/pt/user.json b/snappymail/v/0.0.0/app/localization/pt/user.json index 877f8287d..f9809d736 100644 --- a/snappymail/v/0.0.0/app/localization/pt/user.json +++ b/snappymail/v/0.0.0/app/localization/pt/user.json @@ -11,7 +11,7 @@ "CLOSE": "Fechar", "CONTACTS": "Contactos", "DELETE": "Eliminar", - "UNDELETE": "Undelete", + "UNDELETE": "Restaurar", "DONE": "Terminado", "EMAIL": "Email", "FROM": "De", @@ -77,7 +77,7 @@ "SUBFOLDERS_SUBTREE": "Todas", "SUBFOLDERS_SUBTREE_ONE": "Um nível", "REPLIED": "Respondidas", - "KEYWORD": "Etiqueta" + "KEYWORD": "Marcador" }, "FOLDER_LIST": { "BUTTON_COMPOSE": "Escrever", @@ -89,18 +89,18 @@ "ARCHIVE_NAME": "Arquivo" }, "IMAP": { - "ACL_RIGHTS": "Rights", - "ACL_A": "Administer", - "ACL_E": "Expunge", - "ACL_I": "Insert messages", - "ACL_K": "Create sub-folders", - "ACL_L": "Show messages", - "ACL_P": "Send mail", - "ACL_R": "Select folder", - "ACL_S": "\\SEEN tag", - "ACL_T": "\\DELETED tag", - "ACL_W": "Other tags", - "ACL_X": "Delete\/Rename folder" + "ACL_RIGHTS": "Permissões", + "ACL_A": "Administrar", + "ACL_E": "Purgar", + "ACL_I": "Inserir mensagens", + "ACL_K": "Criar sub-pastas", + "ACL_L": "Mostrar mensagens", + "ACL_P": "Enviar correio", + "ACL_R": "Escolher pasta", + "ACL_S": "Marcador \\SEEN (lida)", + "ACL_T": "Marcador \\DELETED (eliminada)", + "ACL_W": "Outros marcadores", + "ACL_X": "Eliminar\/Renomear pasta" }, "MESSAGE_LIST": { "BUTTON_RELOAD": "Atualizar lista de mensagens", @@ -172,8 +172,8 @@ "LINK_DOWNLOAD_AS_ZIP": "Transferir em arquivo zip", "SPAM_SCORE": "Pontuação de Spam", "HAS_VIRUS_WARNING": "AVISO: vírus detetado", - "TAGS": "Etiquetas", - "NEW_TAG": "Nova etiqueta", + "TAGS": "Marcadores", + "NEW_TAG": "Novo marcador", "ENABLE_TRACKING_LINKS": "Ativar ligações de rastreamento", "TRACKING_ENABLED": "As ligações irão ser rastreadas quando clicadas!" }, @@ -228,7 +228,7 @@ "ADD_MENU_URL": "Website", "ADD_MENU_ADDRESS": "Morada", "ADD_MENU_BIRTHDAY": "Aniversário", - "ADD_MENU_TAGS": "Etiquetas", + "ADD_MENU_TAGS": "Marcadores", "BUTTON_SHARE_ALL": "Todos", "BUTTON_SYNC": "Sincronização (CardDAV)", "SEND_TO_ALL_CONTACT_EMAILS": "Enviar para todos os contactos" @@ -621,7 +621,7 @@ "CantSaveFilters": "Não é possível guardar os filtros", "CantGetFilters": "Não é possível obter os filtros", "CantActivateFiltersScript": "Não é possível ativar o script de filtração", - "CantDeleteFiltersScript": "Não é possível apagar o script de filtração", + "CantDeleteFiltersScript": "Não é possível eliminar o script de filtração", "FiltersAreNotCorrect": "Os filtros não estão corretos", "CantCreateFolder": "Não é possível criar a pasta", "CantRenameFolder": "Não é possível renomear a pasta", From 589606eef0e9e27fee48c0efa4642a510492fd99 Mon Sep 17 00:00:00 2001 From: ner0 <2269603+ner00@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:35:44 +0100 Subject: [PATCH 18/26] SETTINGS_GENERAL/MAILTO --- snappymail/v/0.0.0/app/localization/pt/user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/localization/pt/user.json b/snappymail/v/0.0.0/app/localization/pt/user.json index f9809d736..e93ca5d08 100644 --- a/snappymail/v/0.0.0/app/localization/pt/user.json +++ b/snappymail/v/0.0.0/app/localization/pt/user.json @@ -499,7 +499,7 @@ "IMAGES_OPTION_ALWAYS": "Mostrar sempre", "IMAGES_WHITELIST": "Lista Branca de imagens", "MESSAGE_POPUP_WINDOW": "Abrir em nova janela em vez de um separador", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Registar como manipulador de ligações 'mailto:'" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Adicionar destinatários automaticamente à sua lista de endereços", From 082dd3af413e883cfe1e0dd11af153037c5a30d5 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 27 Aug 2024 23:39:27 +0200 Subject: [PATCH 19/26] Bugfix: Call to a member function Email() on null #1706 --- .../v/0.0.0/app/libraries/RainLoop/ServiceActions.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index 205d9f703..8510bb3bd 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -98,13 +98,15 @@ class ServiceActions $token = Utils::GetCsrfToken(); if (isset($_SERVER['HTTP_X_SM_TOKEN'])) { if ($_SERVER['HTTP_X_SM_TOKEN'] !== $token) { - $sEmail = $this->oActions->getAccountFromToken(false)->Email(); + $oAccount = $this->oActions->getAccountFromToken(false); + $sEmail = $oAccount ? $oAccount->Email() : 'guest'; $this->oActions->logWrite("{$_SERVER['HTTP_X_SM_TOKEN']} !== {$token} for {$sEmail}", \LOG_ERROR, 'Token'); throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'HTTP Token mismatch'); } } else if ($this->oHttp->IsPost()) { if (empty($_POST['XToken']) || $_POST['XToken'] !== $token) { - $sEmail = $this->oActions->getAccountFromToken(false)->Email(); + $oAccount = $this->oActions->getAccountFromToken(false); + $sEmail = $oAccount ? $oAccount->Email() : 'guest'; $this->oActions->logWrite("{$_POST['XToken']} !== {$token} for {$sEmail}", \LOG_ERROR, 'XToken'); throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'XToken mismatch'); } From 6ebf273390413037613a4d0c1aa5244763731a27 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 28 Aug 2024 00:24:28 +0200 Subject: [PATCH 20/26] Resolve #1724 --- dev/Common/Html.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 0557204dd..f3a9d82c7 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -676,7 +676,9 @@ export const forEach('a', a => { let txt = a.textContent, href = a.href; return a.replaceWith( - txt.trim() == href || href.includes('mailto:') ? txt : txt + ' ' + href + ' ' + txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, '')) + ? txt + : txt + ' ' + href + ' ' ); }); From ed7a2bd23981e5df5d34674350c4396165062cbb Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 28 Aug 2024 00:34:38 +0200 Subject: [PATCH 21/26] Resolve #1725 --- snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php | 1 + 1 file changed, 1 insertion(+) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php index bc1abf82b..7ed53df14 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php @@ -299,6 +299,7 @@ class ImapClient extends \MailSo\Net\NetClient private function setCapabilities(ResponseCollection $oResponseCollection) : void { $aList = $oResponseCollection->getCapabilityResult(); + $this->aCapaRaw = $aList; if ($aList) { // Strip unused capabilities $aList = \array_diff($aList, ['PREVIEW=FUZZY', 'SNIPPET=FUZZY', 'SORT=DISPLAY']); From 366ca052007a515a0ff65b1bd15115b4cba88b33 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 30 Aug 2024 00:39:00 +0200 Subject: [PATCH 22/26] Bugfix: application/pgp-encrypted attachments not listed --- .../libraries/MailSo/Imap/BodyStructure.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php index f9adf6497..bf3dc8981 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php @@ -172,9 +172,10 @@ class BodyStructure implements \JsonSerializable ); } - public function IsAttachment() : bool + protected function IsAttachment(/*BodyStructure*/ $oParent) : bool { - return 'application/pgp-encrypted' !== $this->sContentType +// return 'application/pgp-encrypted' !== $this->sContentType + return (!$oParent || !$oParent->isPgpEncrypted()) && ( 'attachment' === $this->sDisposition || ( !\str_starts_with($this->sContentType, 'multipart/') @@ -194,8 +195,8 @@ class BodyStructure implements \JsonSerializable { $aParts = []; - $gParts = $this->SearchByCallback(function ($oItem) { - return $oItem->isText() && !$oItem->IsAttachment(); + $gParts = $this->SearchByCallback(function ($oItem, $oParent) { + return $oItem->isText() && !$oItem->IsAttachment($oParent); }); foreach ($gParts as $oPart) { $aParts[] = $oPart; @@ -232,13 +233,13 @@ class BodyStructure implements \JsonSerializable public function SearchCharset() : string { - $gParts = $this->SearchByCallback(function ($oPart) { - return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment(); + $gParts = $this->SearchByCallback(function ($oPart, $oParent) { + return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment($oParent); }); if (!$gParts->valid()) { - $gParts = $this->SearchByCallback(function ($oPart) { - return $oPart->Charset() && $oPart->IsAttachment(); + $gParts = $this->SearchByCallback(function ($oPart, $oParent) { + return $oPart->Charset() && $oPart->IsAttachment($oParent); }); } @@ -259,7 +260,7 @@ class BodyStructure implements \JsonSerializable { return $this->SearchByCallback(function ($oItem, $oParent) { // return $oItem->IsAttachment(); - return $oItem->IsAttachment() && (!$oParent || !$oParent->isPgpEncrypted()); + return $oItem->IsAttachment($oParent) && (!$oParent || !$oParent->isPgpEncrypted()); }); } From d0fb4ee2f6a89c45b05227d5e214573ba9030208 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 30 Aug 2024 01:47:28 +0200 Subject: [PATCH 23/26] Improved clear cookie handling --- .../libraries/RainLoop/Actions/UserAuth.php | 16 ++-------------- .../0.0.0/app/libraries/snappymail/cookies.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php index 6b2eb99aa..4074a3d3b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -182,18 +182,6 @@ trait UserAuth return $oAccount; } - private static function SetAccountCookie(string $sName, ?Account $oAccount) - { - if ($oAccount) { - Cookies::set( - $sName, - \MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($oAccount)) - ); - } else { - Cookies::clear($sName); - } - } - public function switchAccount(string $sEmail) : bool { $this->Http()->ServerNoCache(); @@ -337,13 +325,13 @@ trait UserAuth public function SetAuthToken(MainAccount $oAccount): void { $this->SetMainAuthAccount($oAccount); - static::SetAccountCookie(self::AUTH_SPEC_TOKEN_KEY, $oAccount); + Cookies::setSecure(self::AUTH_SPEC_TOKEN_KEY, $oAccount); } public function SetAdditionalAuthToken(?AdditionalAccount $oAccount): void { $this->oAdditionalAuthAccount = $oAccount ?: false; - static::SetAccountCookie(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount); + Cookies::setSecure(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount); } /** diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php b/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php index 3ac579b2f..ebe056ae1 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php @@ -47,6 +47,18 @@ class Cookies : null; } + public static function setSecure(string $sName, $data): void + { + if (\is_null($data)) { + static::clear($sName); + } else { + static::set( + $sName, + \MailSo\Base\Utils::UrlSafeBase64Encode(Crypt::EncryptToJSON($data)) + ); + } + } + private static function _set(string $sName, string $sValue, int $iExpire, bool $httponly = true) : bool { $sPath = static::$DefaultPath; @@ -134,5 +146,11 @@ class Cookies { static::init(); static::_set($sName, '', 0); + // Delete 4K split cookie parts + foreach (\array_keys($_COOKIE) as $sCookieName) { + if (\strtok($sCookieName, '~') === $sName) { + static::_set($sCookieName, '', 0); + } + } } } From 0db6c6ab5f9e05c46b13ebbdaf351341710438ac Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 30 Aug 2024 01:50:03 +0200 Subject: [PATCH 24/26] Bugfix: `use_local_proxy_for_external_images` is part of `labs`, not `security` @SonarSource --- snappymail/v/0.0.0/app/libraries/RainLoop/Api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php index 5098824bb..69c5264ff 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php @@ -43,7 +43,7 @@ abstract class Api $CSP->report_only = $oConfig->Get('debug', 'enable', false); // || SNAPPYMAIL_DEV // Allow https: due to remote images in e-mails or use proxy - if (!$oConfig->Get('security', 'use_local_proxy_for_external_images', '')) { + if (!$oConfig->Get('labs', 'use_local_proxy_for_external_images', '')) { $CSP->add('img-src', 'https:'); $CSP->add('img-src', 'http:'); } From 0c9e4717a305315be617a44700ae118ded8ea5d1 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 2 Sep 2024 20:28:03 +0530 Subject: [PATCH 25/26] Remove IsExternal Signed-off-by: Akhil --- dev/Stores/User/App.js | 2 +- plugins/kolab/KolabAddressBook.php | 5 ----- plugins/nextcloud/NextcloudAddressBook.php | 5 ----- snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php | 1 - .../v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php | 1 - .../v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php | 5 ----- .../RainLoop/Providers/AddressBook/AddressBookInterface.php | 2 -- .../RainLoop/Providers/AddressBook/PdoAddressBook.php | 5 ----- 8 files changed, 1 insertion(+), 25 deletions(-) diff --git a/dev/Stores/User/App.js b/dev/Stores/User/App.js index 0a8723c40..9825c8951 100644 --- a/dev/Stores/User/App.js +++ b/dev/Stores/User/App.js @@ -4,7 +4,7 @@ import { ThemeStore } from 'Stores/Theme'; import { arePopupsVisible } from 'Knoin/Knoin'; export const AppUserStore = { - allowContacts: () => !!SettingsGet('contactsAllowed') && !!SettingsGet('contactsExternal') + allowContacts: () => !!SettingsGet('contactsAllowed') }; addObservablesTo(AppUserStore, { diff --git a/plugins/kolab/KolabAddressBook.php b/plugins/kolab/KolabAddressBook.php index 1e01b216a..8c7df9316 100644 --- a/plugins/kolab/KolabAddressBook.php +++ b/plugins/kolab/KolabAddressBook.php @@ -99,11 +99,6 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt return true; } - public function IsExternal(): bool - { - return false; - } - public function SetEmail(string $sEmail) : bool { return true; diff --git a/plugins/nextcloud/NextcloudAddressBook.php b/plugins/nextcloud/NextcloudAddressBook.php index 2790e4e5c..9c84e84ed 100644 --- a/plugins/nextcloud/NextcloudAddressBook.php +++ b/plugins/nextcloud/NextcloudAddressBook.php @@ -38,11 +38,6 @@ class NextcloudAddressBook implements \RainLoop\Providers\AddressBook\AddressBoo return true; } - public function IsExternal(): bool - { - return true; - } - public function SetEmail(string $sEmail) : bool { return true; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 3ebb1de27..eb7356db5 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -678,7 +678,6 @@ class Actions 'accountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]), 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), - 'contactsExternal' => $this->AddressBookProvider($oAccount)->IsExternal(), 'ViewHTML' => (bool) $oConfig->Get('defaults', 'view_html', true), 'ViewImages' => $oConfig->Get('defaults', 'view_images', 'ask'), diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php index 7c9724e7a..7316ddc2e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -202,7 +202,6 @@ trait Accounts 'accountHash' => $oAccount->Hash(), 'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(), 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), - 'contactsExternal' => $this->AddressBookProvider($oAccount)->IsExternal(), 'HideUnsubscribed' => false, 'UseThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false), 'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false), diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php index 332836449..affaecd99 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php @@ -25,11 +25,6 @@ class AddressBook extends AbstractProvider return $this->oDriver && $this->oDriver->IsSupported(); } - public function IsExternal() : bool - { - return $this->IsActive() ? $this->oDriver->IsExternal() : false; - } - public function Sync() : bool { return $this->IsActive() ? $this->oDriver->Sync() : false; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php index 50d773244..564a0d48b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php @@ -6,8 +6,6 @@ interface AddressBookInterface { public function IsSupported() : bool; - public function IsExternal() : bool; - public function SetEmail(string $sEmail) : bool; public function Sync() : bool; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php index 76af282f3..3c60f44f3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php @@ -75,11 +75,6 @@ class PdoAddressBook $aDrivers = static::getAvailableDrivers(); return \is_array($aDrivers) && \in_array($this->settings->driver, $aDrivers); } - - public function IsExternal(): bool - { - return false; - } public function SetEmail(string $sEmail) : bool { From 022bf430ffcf687c4307dc94395fdaa370443aaf Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 2 Sep 2024 20:30:03 +0530 Subject: [PATCH 26/26] Remove extra space Signed-off-by: Akhil --- .../libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php index 3c60f44f3..fbf5aeebc 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php @@ -75,7 +75,7 @@ class PdoAddressBook $aDrivers = static::getAvailableDrivers(); return \is_array($aDrivers) && \in_array($this->settings->driver, $aDrivers); } - + public function SetEmail(string $sEmail) : bool { $this->iUserID = $this->getUserId($sEmail);
    Extension Loaded