From 34e4bad77fb9ca9a96113ca96cbfb300812c76ec Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 16 Jan 2024 15:38:27 +0100 Subject: [PATCH 001/540] Workaround #1392 --- plugins/avatars/index.php | 6 +++--- .../v/0.0.0/app/libraries/snappymail/file/mimetype.php | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 4d1f67562..06fb2e30e 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.12', - RELEASE = '2023-11-27', + VERSION = '1.13', + RELEASE = '2024-01-16', REQUIRED = '2.25.0', CATEGORY = 'Contacts', LICENSE = 'MIT', @@ -316,7 +316,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin if ($aFiles) { \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0])); return [ - \mime_content_type($aFiles[0]), + \SnappyMail\File\MimeType::fromFile($aFiles[0]), \file_get_contents($aFiles[0]) ]; } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php b/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php index 8d41bf681..e64b57293 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php @@ -35,6 +35,9 @@ abstract class MimeType if (self::$finfo) { $mime = \preg_replace('#[,;].*#', '', self::$finfo->file($filename)); } + if (!$mime && \is_callable('mime_content_type')) { + $mime = \mime_content_type($filename); + } if (!$mime && $fp = \fopen($filename, 'rb')) { $mime = self::fromStream($fp); \fclose($fp); From 816a2745bc6319820e32119dba48cac49828514c Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 16 Jan 2024 15:48:47 +0100 Subject: [PATCH 002/540] Resolve #1392 --- snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php | 2 +- snappymail/v/0.0.0/app/libraries/snappymail/integrity.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 5b8cef13d..6e4613fec 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php @@ -290,7 +290,7 @@ class ActionsAdmin extends Actions ] ]; - foreach (['APCu', 'cURL','GnuPG','GD','Gmagick','Imagick','iconv','intl','LDAP','OpenSSL','pdo_mysql','pdo_pgsql','pdo_sqlite','redis','Sodium','Tidy','uuid','XXTEA','Zip'] as $name) { + foreach (['APCu', 'cURL','Fileinfo','GnuPG','GD','Gmagick','Imagick','iconv','intl','LDAP','OpenSSL','pdo_mysql','pdo_pgsql','pdo_sqlite','redis','Sodium','Tidy','uuid','XXTEA','Zip'] as $name) { $aResult['php'][] = [ 'name' => ('OpenSSL' === $name && \defined('OPENSSL_VERSION_TEXT')) ? OPENSSL_VERSION_TEXT : $name, 'loaded' => \extension_loaded(\strtolower($name)), diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php b/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php index 66155727a..be4e8327f 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/integrity.php @@ -103,7 +103,9 @@ abstract class Integrity 'ctype' => extension_loaded('ctype'), 'json' => function_exists('json_decode'), 'libxml' => function_exists('libxml_use_internal_errors'), - 'dom' => class_exists('DOMDocument') + 'dom' => class_exists('DOMDocument'), + // https://github.com/the-djmaze/snappymail/issues/1392 + 'fileinfo' => extension_loaded('fileinfo') // https://github.com/the-djmaze/snappymail/issues/392 // 'phar' => class_exists('PharData') ); From 8f9c19936049574eed365c85726ff655c1e21109 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 16 Jan 2024 15:49:15 +0100 Subject: [PATCH 003/540] Bump Nextcloud extension version --- plugins/nextcloud/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index cabd8a474..96a9e27e5 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -4,8 +4,8 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Nextcloud', - VERSION = '2.28', - RELEASE = '2023-12-17', + VERSION = '2.29', + RELEASE = '2024-01-12', CATEGORY = 'Integrations', DESCRIPTION = 'Integrate with Nextcloud v20+', REQUIRED = '2.27.0'; From e965d58de04eca399ce45bcafdc0d4f2293167f0 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 16 Jan 2024 16:08:22 +0100 Subject: [PATCH 004/540] Resolve #1140 --- .../app/libraries/RainLoop/ActionsAdmin.php | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) 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 6e4613fec..5d4ea5bf3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php @@ -290,24 +290,59 @@ class ActionsAdmin extends Actions ] ]; - foreach (['APCu', 'cURL','Fileinfo','GnuPG','GD','Gmagick','Imagick','iconv','intl','LDAP','OpenSSL','pdo_mysql','pdo_pgsql','pdo_sqlite','redis','Sodium','Tidy','uuid','XXTEA','Zip'] as $name) { + foreach (['APCu', 'cURL','Fileinfo','iconv','intl','LDAP','redis','Tidy','uuid','Zip'] as $name) { $aResult['php'][] = [ - 'name' => ('OpenSSL' === $name && \defined('OPENSSL_VERSION_TEXT')) ? OPENSSL_VERSION_TEXT : $name, + 'name' => $name, 'loaded' => \extension_loaded(\strtolower($name)), 'version' => \phpversion($name) ]; } - $aResult['php'][] = [ - 'name' => 'Fileinfo', - 'loaded' => \class_exists('finfo'), - 'version' => \phpversion('fileinfo') - ]; + $aResult['php'][] = [ 'name' => 'Phar', 'loaded' => \class_exists('PharData'), 'version' => \phpversion('phar') ]; + $aResult['php'][] = [ + 'name' => 'Contacts database:', + 'loaded' => \extension_loaded('pdo_mysql') || \extension_loaded('pdo_pgsql') || \extension_loaded('pdo_sqlite'), + 'version' => 0 + ]; + foreach (['pdo_mysql','pdo_pgsql','pdo_sqlite'] as $name) { + $aResult['php'][] = [ + 'name' => "- {$name}", + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + + $aResult['php'][] = [ + 'name' => 'Crypt:', + 'loaded' => true, + 'version' => 0 + ]; + foreach (['Sodium','OpenSSL','XXTEA','GnuPG'] as $name) { + $aResult['php'][] = [ + 'name' => '- ' . (('OpenSSL' === $name && \defined('OPENSSL_VERSION_TEXT')) ? OPENSSL_VERSION_TEXT : $name), + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + + $aResult['php'][] = [ + 'name' => 'Image processing:', + 'loaded' => \extension_loaded('gd') || \extension_loaded('gmagick') || \extension_loaded('imagick'), + 'version' => 0 + ]; + foreach (['GD','Gmagick','Imagick'] as $name) { + $aResult['php'][] = [ + 'name' => "- {$name}", + 'loaded' => \extension_loaded(\strtolower($name)), + 'version' => \phpversion($name) + ]; + } + return $this->DefaultResponse($aResult); } From b89a1b309cfe2cbc3230d8d132ef80fd4754dc07 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 17 Jan 2024 00:12:01 +0100 Subject: [PATCH 005/540] Bugfix: dataBaseUpgrade always runs on sqlite and pgsql And split the db schema data into seperate class --- .../0.0.0/app/libraries/RainLoop/Pdo/Base.php | 101 +++-------- .../app/libraries/RainLoop/Pdo/Schema.php | 68 +++++++ .../Providers/AddressBook/PdoAddressBook.php | 159 +--------------- .../Providers/AddressBook/PdoSchema.php | 170 ++++++++++++++++++ 4 files changed, 268 insertions(+), 230 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php create mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php index 08a57f779..2becb01ff 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php @@ -257,51 +257,7 @@ abstract class Base $oPdo = $this->getPDO(); if ($oPdo) { - $aQ = array(); - switch ($this->sDbType) - { - case 'mysql': - $aQ[] = 'CREATE TABLE IF NOT EXISTS rainloop_system ( -sys_name varchar(64) NOT NULL, -value_int int UNSIGNED NOT NULL DEFAULT 0, -PRIMARY KEY (sys_name) -) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;'; - $aQ[] = 'CREATE TABLE IF NOT EXISTS rainloop_users ( -id_user int UNSIGNED NOT NULL AUTO_INCREMENT, -rl_email varchar(254) NOT NULL, -PRIMARY KEY (id_user), -UNIQUE KEY ui_rainloop_users_email (rl_email) -);'; - break; - - case 'pgsql': - $aQ[] = 'CREATE TABLE rainloop_system ( -sys_name varchar(50) NOT NULL, -value_int integer NOT NULL DEFAULT 0 -);'; - $aQ[] = 'CREATE INDEX sys_name_rainloop_system_index ON rainloop_system (sys_name);'; - $aQ[] = 'CREATE SEQUENCE id_user START WITH 1 INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1;'; - $aQ[] = 'CREATE TABLE rainloop_users ( -id_user integer DEFAULT nextval(\'id_user\'::text) PRIMARY KEY, -rl_email varchar(254) NOT NULL DEFAULT \'\' -);'; - $aQ[] = 'CREATE INDEX rl_email_rainloop_users_index ON rainloop_users (rl_email);'; - break; - - case 'sqlite': - $aQ[] = 'CREATE TABLE rainloop_system ( -sys_name text NOT NULL, -value_int integer NOT NULL DEFAULT 0 -);'; - $aQ[] = 'CREATE UNIQUE INDEX ui_rainloop_system_sys_name ON rainloop_system (sys_name);'; - $aQ[] = 'CREATE TABLE rainloop_users ( -id_user integer NOT NULL PRIMARY KEY, -rl_email text NOT NULL DEFAULT \'\' -);'; - $aQ[] = 'CREATE INDEX rl_email_rainloop_users_index ON rainloop_users (rl_email);'; - break; - } - + $aQ = Schema::getForDbType($this->sDbType); if (\count($aQ)) { try { @@ -352,41 +308,36 @@ rl_email text NOT NULL DEFAULT \'\' $bResult = false; if (\is_int($iFromVersion) && 0 <= $iFromVersion) { $oPdo = false; - foreach ($aData as $iVersion => $aQuery) { - if (0 === \count($aQuery)) { - continue; - } - - if (!$oPdo) { - $oPdo = $this->getPDO(); - $bResult = true; - } - - if ($iFromVersion < $iVersion && $oPdo) { - try - { - foreach ($aQuery as $sQuery) { - $this->writeLog($sQuery); - $bExec = $oPdo->exec($sQuery); - if (false === $bExec) { - $this->writeLog('Result: false'); - - $bResult = false; + if ($iFromVersion < $iVersion) { + if (\count($aQuery)) { + if (!$oPdo) { + $oPdo = $this->getPDO(); + $bResult = true; + } + if ($oPdo) { + try + { + foreach ($aQuery as $sQuery) { + $this->writeLog($sQuery); + $bExec = $oPdo->exec($sQuery); + if (false === $bExec) { + $this->writeLog('Result: false'); + $bResult = false; + break; + } + } + } + catch (\Throwable $oException) + { + $this->writeLog($oException); + throw $oException; + } + if (!$bResult) { break; } } } - catch (\Throwable $oException) - { - $this->writeLog($oException); - throw $oException; - } - - if (!$bResult) { - break; - } - $this->setVersion($sName, $iVersion); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php new file mode 100644 index 000000000..1a34d96ef --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Schema.php @@ -0,0 +1,68 @@ +settings->driver) { case 'mysql': - $mCache = $this->dataBaseUpgrade($this->settings->driver.'-ab-version', array( - 1 => $this->getInitialTablesArray($this->settings->driver), - 2 => array( -'ALTER TABLE rainloop_ab_properties ADD prop_value_lower MEDIUMTEXT NOT NULL AFTER prop_value_custom;' - ), - 3 => array( -'ALTER TABLE rainloop_ab_properties CHANGE prop_value prop_value MEDIUMTEXT NOT NULL;', -'ALTER TABLE rainloop_ab_properties CHANGE prop_value_custom prop_value_custom MEDIUMTEXT NOT NULL;', -'ALTER TABLE rainloop_ab_properties CHANGE prop_value_lower prop_value_lower MEDIUMTEXT NOT NULL;' - ), - 4 => array( -'ALTER TABLE rainloop_ab_properties CHANGE prop_value prop_value MEDIUMTEXT NOT NULL;', -'ALTER TABLE rainloop_ab_properties CHANGE prop_value_custom prop_value_custom MEDIUMTEXT NOT NULL;', -'ALTER TABLE rainloop_ab_properties CHANGE prop_value_lower prop_value_lower MEDIUMTEXT NOT NULL;' - ) - )); - break; case 'pgsql': - $mCache = $this->dataBaseUpgrade($this->settings->driver.'-ab-version', array( - 1 => $this->getInitialTablesArray($this->settings->driver), - 2 => array( -'ALTER TABLE rainloop_ab_properties ADD prop_value_lower text NOT NULL DEFAULT \'\';' - ), - 3 => array(), - 4 => array() - )); - break; case 'sqlite': - $mCache = $this->dataBaseUpgrade($this->settings->driver.'-ab-version', array( - 1 => $this->getInitialTablesArray($this->settings->driver), - 2 => array( -'ALTER TABLE rainloop_ab_properties ADD prop_value_lower text NOT NULL DEFAULT \'\';' - ), - 3 => array(), - 4 => array() - )); + $mCache = $this->dataBaseUpgrade( + $this->settings->driver.'-ab-version', + PdoSchema::getForDbType($this->settings->driver) + ); break; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php new file mode 100644 index 000000000..8f97f84d2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoSchema.php @@ -0,0 +1,170 @@ + [], + 2 => [ +'ALTER TABLE rainloop_ab_properties ADD prop_value_lower MEDIUMTEXT NOT NULL AFTER prop_value_custom;' + ], + 3 => [ +'ALTER TABLE rainloop_ab_properties CHANGE prop_value prop_value MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_custom prop_value_custom MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_lower prop_value_lower MEDIUMTEXT NOT NULL;' + ], + 4 => [ +'ALTER TABLE rainloop_ab_properties CHANGE prop_value prop_value MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_custom prop_value_custom MEDIUMTEXT NOT NULL;', +'ALTER TABLE rainloop_ab_properties CHANGE prop_value_lower prop_value_lower MEDIUMTEXT NOT NULL;' + ] + ]; + break; + + case 'pgsql': + $aVersions = [ + 1 => [], + 2 => [ +'ALTER TABLE rainloop_ab_properties ADD prop_value_lower text NOT NULL DEFAULT \'\';' + ] + ]; + break; + + case 'sqlite': + $aVersions = [ + 1 => [], + 2 => [ +'ALTER TABLE rainloop_ab_properties ADD prop_value_lower text NOT NULL DEFAULT \'\';' + ] + ]; + $sInitial = static::{$sDbType}(); + break; + } + + if ($aVersions) { + $aList = \explode(';', \trim(static::{$sDbType}())); + foreach ($aList as $sV) { + $sV = \trim($sV); + if (\strlen($sV)) { + $aVersions[1][] = $sV; + } + } + } + + return $aVersions; + } + +} From 2d829178f896f92c33be0e7bd205b02526176e05 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 17 Jan 2024 00:55:23 +0100 Subject: [PATCH 006/540] Added feature to use the SQLite AddressBook per login account instead of global. On by default unless the global AddressBook.sqlite already exists. --- dev/Settings/Admin/Contacts.js | 5 +++- .../0.0.0/app/libraries/RainLoop/Actions.php | 1 + .../app/libraries/RainLoop/ActionsAdmin.php | 3 +++ .../libraries/RainLoop/Config/Application.php | 1 + .../Providers/AddressBook/PdoAddressBook.php | 24 +++++++++---------- .../v/0.0.0/app/localization/cs-CZ/admin.json | 1 + .../v/0.0.0/app/localization/da-DK/admin.json | 1 + .../v/0.0.0/app/localization/de-DE/admin.json | 1 + .../v/0.0.0/app/localization/en/admin.json | 1 + .../v/0.0.0/app/localization/es-ES/admin.json | 1 + .../v/0.0.0/app/localization/eu/admin.json | 1 + .../v/0.0.0/app/localization/fa-IR/admin.json | 1 + .../v/0.0.0/app/localization/fi-FI/admin.json | 1 + .../v/0.0.0/app/localization/fr-FR/admin.json | 1 + .../v/0.0.0/app/localization/hu-HU/admin.json | 1 + .../v/0.0.0/app/localization/id-ID/admin.json | 1 + .../v/0.0.0/app/localization/it-IT/admin.json | 1 + .../v/0.0.0/app/localization/ja-JP/admin.json | 1 + .../v/0.0.0/app/localization/lt-LT/admin.json | 1 + .../v/0.0.0/app/localization/nb-NO/admin.json | 1 + .../v/0.0.0/app/localization/nl-NL/admin.json | 1 + .../v/0.0.0/app/localization/nl-NL/user.json | 2 +- .../v/0.0.0/app/localization/pl-PL/admin.json | 1 + .../v/0.0.0/app/localization/pt-BR/admin.json | 1 + .../v/0.0.0/app/localization/pt-PT/admin.json | 1 + .../v/0.0.0/app/localization/pt/admin.json | 1 + .../v/0.0.0/app/localization/ru-RU/admin.json | 1 + .../v/0.0.0/app/localization/sk-SK/admin.json | 1 + .../v/0.0.0/app/localization/sl-SI/admin.json | 1 + .../v/0.0.0/app/localization/sv-SE/admin.json | 1 + .../v/0.0.0/app/localization/vi-VN/admin.json | 1 + .../v/0.0.0/app/localization/zh-CN/admin.json | 1 + .../Views/Admin/AdminSettingsContacts.html | 17 ++++++------- 33 files changed, 57 insertions(+), 22 deletions(-) diff --git a/dev/Settings/Admin/Contacts.js b/dev/Settings/Admin/Contacts.js index b05d5ab3b..ad65c8453 100644 --- a/dev/Settings/Admin/Contacts.js +++ b/dev/Settings/Admin/Contacts.js @@ -28,6 +28,8 @@ export class AdminSettingsContacts extends AbstractViewSettings { this.addSetting('contactsMySQLSSLVerify'); this.addSetting('contactsMySQLSSLCiphers'); + this.addSetting('contactsSQLiteGlobal'); + addObservablesTo(this, { testing: false, testContactsSuccess: false, @@ -102,7 +104,8 @@ export class AdminSettingsContacts extends AbstractViewSettings { PdoPassword: this.contactsPdoPassword(), MySQLSSLCA: this.contactsMySQLSSLCA(), MySQLSSLVerify: this.contactsMySQLSSLVerify(), - MySQLSSLCiphers: this.contactsMySQLSSLCiphers() + MySQLSSLCiphers: this.contactsMySQLSSLCiphers(), + SQLiteGlobal: this.contactsSQLiteGlobal() } ); } 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 42b56f9c3..10535e2d1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -649,6 +649,7 @@ class Actions $aResult['contactsMySQLSSLCA'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ca', ''); $aResult['contactsMySQLSSLVerify'] = !!$oConfig->Get('contacts', 'mysql_ssl_verify', true); $aResult['contactsMySQLSSLCiphers'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ciphers', ''); + $aResult['contactsSQLiteGlobal'] = !!$oConfig->Get('contacts', 'sqlite_global', \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')); $aResult['contactsSuggestionsLimit'] = (int)$oConfig->Get('contacts', 'suggestions_limit', 20); $aResult['faviconUrl'] = $oConfig->Get('webmail', 'favicon_url', ''); 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 5d4ea5bf3..17d81eed9 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php @@ -76,6 +76,7 @@ class ActionsAdmin extends Actions $this->setConfigFromParams($oConfig, 'contactsMySQLSSLCA', 'contacts', 'mysql_ssl_ca', 'string'); $this->setConfigFromParams($oConfig, 'contactsMySQLSSLVerify', 'contacts', 'mysql_ssl_verify', 'bool'); $this->setConfigFromParams($oConfig, 'contactsMySQLSSLCiphers', 'contacts', 'mysql_ssl_ciphers', 'string'); + $this->setConfigFromParams($oConfig, 'contactsSQLiteGlobal', 'contacts', 'sqlite_global', 'bool'); $this->setConfigFromParams($oConfig, 'contactsSuggestionsLimit', 'contacts', 'suggestions_limit', 'int'); $this->setConfigFromParams($oConfig, 'contactsPdoType', 'contacts', 'type', 'string', function ($sType) use ($self) { return Providers\AddressBook\PdoAddressBook::validPdoType($sType); @@ -153,6 +154,7 @@ class ActionsAdmin extends Actions $this->setConfigFromParams($oConfig, 'MySQLSSLCA', 'contacts', 'mysql_ssl_ca', 'string'); $this->setConfigFromParams($oConfig, 'MySQLSSLVerify', 'contacts', 'mysql_ssl_verify', 'bool'); $this->setConfigFromParams($oConfig, 'MySQLSSLCiphers', 'contacts', 'mysql_ssl_ciphers', 'string'); + $this->setConfigFromParams($oConfig, 'SQLiteGlobal', 'contacts', 'sqlite_global', 'bool'); $sTestMessage = ''; try { @@ -391,6 +393,7 @@ class ActionsAdmin extends Actions $aResult['contactsMySQLSSLCA'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ca', ''); $aResult['contactsMySQLSSLVerify'] = !!$oConfig->Get('contacts', 'mysql_ssl_verify', true); $aResult['contactsMySQLSSLCiphers'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ciphers', ''); + $aResult['contactsSQLiteGlobal'] = !!$oConfig->Get('contacts', 'sqlite_global', \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')); $aResult['contactsSuggestionsLimit'] = (int)$oConfig->Get('contacts', 'suggestions_limit', 20); $aResult['faviconUrl'] = $oConfig->Get('webmail', 'favicon_url', ''); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php index 29773aec8..1dcdd3fe0 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php @@ -201,6 +201,7 @@ Warning: only enable when server does not do this, else double compression error 'mysql_ssl_ca' => array('', 'PEM format certificate'), 'mysql_ssl_verify' => array(true), 'mysql_ssl_ciphers' => array('', 'HIGH'), + 'sqlite_global' => array(\is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')), 'suggestions_limit' => array(20) ), 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 d9caca409..2c26073fd 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 @@ -32,20 +32,20 @@ class PdoAddressBook $oSettings->driver = static::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); if ('sqlite' === $oSettings->driver) { $sDsn = 'sqlite:' . APP_PRIVATE_DATA . 'AddressBook.sqlite'; -/* - // TODO: use local db? - $oAccount = \RainLoop\Api::Actions()->getMainAccountFromToken(false); - if ($oAccount) { - $homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath( - $oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT - ); - if (!\is_file($homedir . 'AddressBook.sqlite') && \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')) { - \copy(APP_PRIVATE_DATA . '/AddressBook.sqlite', $homedir . 'AddressBook.sqlite'); + if (!$oConfig->Get('contacts', 'sqlite_global', \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite'))) { + $oAccount = \RainLoop\Api::Actions()->getMainAccountFromToken(false); + if ($oAccount) { + $homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT + ); + // TODO: sync data on switch? +// if (!\is_file($homedir . 'AddressBook.sqlite') && \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')) { +// \copy(APP_PRIVATE_DATA . '/AddressBook.sqlite', $homedir . 'AddressBook.sqlite'); +// } + $sDsn = 'sqlite:' . $homedir . 'AddressBook.sqlite'; } - $sDsn = 'sqlite:' . $homedir . 'AddressBook.sqlite'; } -*/ } else { $sDsn = \trim($oConfig->Get('contacts', 'pdo_dsn', '')); $oSettings->user = \trim($oConfig->Get('contacts', 'pdo_user', '')); diff --git a/snappymail/v/0.0.0/app/localization/cs-CZ/admin.json b/snappymail/v/0.0.0/app/localization/cs-CZ/admin.json index 68f8dce38..3698b0538 100644 --- a/snappymail/v/0.0.0/app/localization/cs-CZ/admin.json +++ b/snappymail/v/0.0.0/app/localization/cs-CZ/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Uživatel", "LABEL_STORAGE_PASSWORD": "Heslo", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Upozornění!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Nepoužívejte tento typ databáze při velkém počtu aktivních uživatelů.", diff --git a/snappymail/v/0.0.0/app/localization/da-DK/admin.json b/snappymail/v/0.0.0/app/localization/da-DK/admin.json index cf90f9b6a..95e7b1163 100644 --- a/snappymail/v/0.0.0/app/localization/da-DK/admin.json +++ b/snappymail/v/0.0.0/app/localization/da-DK/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Bruger", "LABEL_STORAGE_PASSWORD": "Adgangskode", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Underretning!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Brug ikke denne database type med større antal af aktive brugere.", diff --git a/snappymail/v/0.0.0/app/localization/de-DE/admin.json b/snappymail/v/0.0.0/app/localization/de-DE/admin.json index 13393e717..a576ca8b8 100644 --- a/snappymail/v/0.0.0/app/localization/de-DE/admin.json +++ b/snappymail/v/0.0.0/app/localization/de-DE/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "DSN", "LABEL_STORAGE_USER": "Benutzer", "LABEL_STORAGE_PASSWORD": "Passwort", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Hinweis!", "SUGGESTIONS_LIMIT": "Limit für Empfehlungen", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Verwenden Sie diesen Datenbanktyp nicht bei einer hohen Anzahl aktiver Benutzer.", diff --git a/snappymail/v/0.0.0/app/localization/en/admin.json b/snappymail/v/0.0.0/app/localization/en/admin.json index 890698e23..5032add7c 100644 --- a/snappymail/v/0.0.0/app/localization/en/admin.json +++ b/snappymail/v/0.0.0/app/localization/en/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Data Source Name (DSN)", "LABEL_STORAGE_USER": "User", "LABEL_STORAGE_PASSWORD": "Password", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Notice!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Don't use this database type with a large number of active users.", diff --git a/snappymail/v/0.0.0/app/localization/es-ES/admin.json b/snappymail/v/0.0.0/app/localization/es-ES/admin.json index 3293bd14a..c824cd7ca 100644 --- a/snappymail/v/0.0.0/app/localization/es-ES/admin.json +++ b/snappymail/v/0.0.0/app/localization/es-ES/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Usuario", "LABEL_STORAGE_PASSWORD": "Contraseña", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "¡Advertencia!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "No utilices este tipo de Base de Datos para un número grande de usuarios.", diff --git a/snappymail/v/0.0.0/app/localization/eu/admin.json b/snappymail/v/0.0.0/app/localization/eu/admin.json index 54176219b..a06de95f7 100644 --- a/snappymail/v/0.0.0/app/localization/eu/admin.json +++ b/snappymail/v/0.0.0/app/localization/eu/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Erabiltzailea", "LABEL_STORAGE_PASSWORD": "Pasahitza", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Adi!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Ez erabili datubase hau erabiltzaile aktibo koputu handiarekin.", diff --git a/snappymail/v/0.0.0/app/localization/fa-IR/admin.json b/snappymail/v/0.0.0/app/localization/fa-IR/admin.json index 50a582025..e683f1f2e 100644 --- a/snappymail/v/0.0.0/app/localization/fa-IR/admin.json +++ b/snappymail/v/0.0.0/app/localization/fa-IR/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "کاربر", "LABEL_STORAGE_PASSWORD": "گذرواژه", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "توجه!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "از این پایگاه داده برای کاربران زیاد استفاده نکنید.", diff --git a/snappymail/v/0.0.0/app/localization/fi-FI/admin.json b/snappymail/v/0.0.0/app/localization/fi-FI/admin.json index d29e7b775..3d82e3cd5 100644 --- a/snappymail/v/0.0.0/app/localization/fi-FI/admin.json +++ b/snappymail/v/0.0.0/app/localization/fi-FI/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Käyttäjä", "LABEL_STORAGE_PASSWORD": "Salasana", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Huomio!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Älä käytä tätä tietokantatyyppiä suurella määrällä aktiivisia käyttäjiä.", diff --git a/snappymail/v/0.0.0/app/localization/fr-FR/admin.json b/snappymail/v/0.0.0/app/localization/fr-FR/admin.json index a82855989..ad043e8f8 100644 --- a/snappymail/v/0.0.0/app/localization/fr-FR/admin.json +++ b/snappymail/v/0.0.0/app/localization/fr-FR/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Utilisateur", "LABEL_STORAGE_PASSWORD": "Mot de passe", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Note !", "SUGGESTIONS_LIMIT": "Limite de suggestions", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "N'utilisez pas ce type de base de données avec un grand nombre d'utilisateurs actifs.", diff --git a/snappymail/v/0.0.0/app/localization/hu-HU/admin.json b/snappymail/v/0.0.0/app/localization/hu-HU/admin.json index df7d58067..7355a96e5 100644 --- a/snappymail/v/0.0.0/app/localization/hu-HU/admin.json +++ b/snappymail/v/0.0.0/app/localization/hu-HU/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "DSN", "LABEL_STORAGE_USER": "Felhasználó", "LABEL_STORAGE_PASSWORD": "Jelszó", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Megjegyzés!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Ne használd ezt az adatbázis típust nagy számú aktív felhasználóval.", diff --git a/snappymail/v/0.0.0/app/localization/id-ID/admin.json b/snappymail/v/0.0.0/app/localization/id-ID/admin.json index c3ed39b8b..df50bc842 100644 --- a/snappymail/v/0.0.0/app/localization/id-ID/admin.json +++ b/snappymail/v/0.0.0/app/localization/id-ID/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "User", "LABEL_STORAGE_PASSWORD": "Password", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Perhatian!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Jangan gunakan tipe database ini untuk jumlah user aktif yang besar.", diff --git a/snappymail/v/0.0.0/app/localization/it-IT/admin.json b/snappymail/v/0.0.0/app/localization/it-IT/admin.json index 928c4cb5f..5f8240f28 100644 --- a/snappymail/v/0.0.0/app/localization/it-IT/admin.json +++ b/snappymail/v/0.0.0/app/localization/it-IT/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Utente", "LABEL_STORAGE_PASSWORD": "Password", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Avviso!", "SUGGESTIONS_LIMIT": "Limite suggerimenti", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Non usare questo tipo di database con un grande numero di utenti attivi.", diff --git a/snappymail/v/0.0.0/app/localization/ja-JP/admin.json b/snappymail/v/0.0.0/app/localization/ja-JP/admin.json index c8e8487c4..3917ed091 100644 --- a/snappymail/v/0.0.0/app/localization/ja-JP/admin.json +++ b/snappymail/v/0.0.0/app/localization/ja-JP/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "ユーザー", "LABEL_STORAGE_PASSWORD": "パスワード", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "注意!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "アクティブユーザーの数が多いとき、このデータベース・タイプを使用しないでください。", diff --git a/snappymail/v/0.0.0/app/localization/lt-LT/admin.json b/snappymail/v/0.0.0/app/localization/lt-LT/admin.json index 42d2e14be..a27097da2 100644 --- a/snappymail/v/0.0.0/app/localization/lt-LT/admin.json +++ b/snappymail/v/0.0.0/app/localization/lt-LT/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Vartotojas", "LABEL_STORAGE_PASSWORD": "Slaptažodis", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Pastaba!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Nenaudokite šio duomenų bazės tipo, jei turite daug aktyvių vartotojų", diff --git a/snappymail/v/0.0.0/app/localization/nb-NO/admin.json b/snappymail/v/0.0.0/app/localization/nb-NO/admin.json index 12eb31321..71d356fab 100644 --- a/snappymail/v/0.0.0/app/localization/nb-NO/admin.json +++ b/snappymail/v/0.0.0/app/localization/nb-NO/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Bruker", "LABEL_STORAGE_PASSWORD": "Passord", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Varsel!", "SUGGESTIONS_LIMIT": "Forslagsbegrensning", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Ikke bruk denne databasetypen hvis dette systemet har mange brukere.", diff --git a/snappymail/v/0.0.0/app/localization/nl-NL/admin.json b/snappymail/v/0.0.0/app/localization/nl-NL/admin.json index c16f41000..7708260fa 100644 --- a/snappymail/v/0.0.0/app/localization/nl-NL/admin.json +++ b/snappymail/v/0.0.0/app/localization/nl-NL/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Gebruikersnaam", "LABEL_STORAGE_PASSWORD": "Wachtwoord", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Aandacht!", "SUGGESTIONS_LIMIT": "Suggesties limiet", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Gegruik deze database soort niet met een groot aantal actieve gebrukers.", diff --git a/snappymail/v/0.0.0/app/localization/nl-NL/user.json b/snappymail/v/0.0.0/app/localization/nl-NL/user.json index 53d03dca4..8aeabbcb3 100644 --- a/snappymail/v/0.0.0/app/localization/nl-NL/user.json +++ b/snappymail/v/0.0.0/app/localization/nl-NL/user.json @@ -255,7 +255,7 @@ "TITLE_UPDATE_IDENTITY": "Identiteit bijwerken?", "BUTTON_ADD_IDENTITY": "Toevoegen", "BUTTON_UPDATE_IDENTITY": "Bijwerken", - "LABEL_SIGNATURE_ADD": "Handtekening toevoegen/wijzigen", + "LABEL_SIGNATURE_ADD": "Handtekening toevoegen\/wijzigen", "LABEL_SIGNATURE_INSERT_BEFORE": "Plaats deze handtekening vóór geciteerde tekst in antwoorden" }, "POPUPS_CREATE_FOLDER": { diff --git a/snappymail/v/0.0.0/app/localization/pl-PL/admin.json b/snappymail/v/0.0.0/app/localization/pl-PL/admin.json index 239a2c473..a214fe4f5 100644 --- a/snappymail/v/0.0.0/app/localization/pl-PL/admin.json +++ b/snappymail/v/0.0.0/app/localization/pl-PL/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Adres DSN", "LABEL_STORAGE_USER": "Użytkownik", "LABEL_STORAGE_PASSWORD": "Hasło", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Ostrzeżenie!", "SUGGESTIONS_LIMIT": "Limit sugestii", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Nie używaj tej bazy danych z dużą ilością aktywnych użytkowników.", diff --git a/snappymail/v/0.0.0/app/localization/pt-BR/admin.json b/snappymail/v/0.0.0/app/localization/pt-BR/admin.json index 0d40087e9..df8d04c66 100644 --- a/snappymail/v/0.0.0/app/localization/pt-BR/admin.json +++ b/snappymail/v/0.0.0/app/localization/pt-BR/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Usuário", "LABEL_STORAGE_PASSWORD": "Senha", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Aviso!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Não use este tipo de banco de dados com um grande número de usuários ativos.", diff --git a/snappymail/v/0.0.0/app/localization/pt-PT/admin.json b/snappymail/v/0.0.0/app/localization/pt-PT/admin.json index 9c87092b9..68b9a33aa 100644 --- a/snappymail/v/0.0.0/app/localization/pt-PT/admin.json +++ b/snappymail/v/0.0.0/app/localization/pt-PT/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Nome da Origem de Dados (DSN)", "LABEL_STORAGE_USER": "Utilizador", "LABEL_STORAGE_PASSWORD": "Palavra-passe", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Aviso!", "SUGGESTIONS_LIMIT": "Limite de sugestões", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Não use este tipo de base de dados com uma grande quantidade de utilizadores ativos.", diff --git a/snappymail/v/0.0.0/app/localization/pt/admin.json b/snappymail/v/0.0.0/app/localization/pt/admin.json index 9c87092b9..68b9a33aa 100644 --- a/snappymail/v/0.0.0/app/localization/pt/admin.json +++ b/snappymail/v/0.0.0/app/localization/pt/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Nome da Origem de Dados (DSN)", "LABEL_STORAGE_USER": "Utilizador", "LABEL_STORAGE_PASSWORD": "Palavra-passe", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Aviso!", "SUGGESTIONS_LIMIT": "Limite de sugestões", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Não use este tipo de base de dados com uma grande quantidade de utilizadores ativos.", diff --git a/snappymail/v/0.0.0/app/localization/ru-RU/admin.json b/snappymail/v/0.0.0/app/localization/ru-RU/admin.json index 5df14f911..2152a41e2 100644 --- a/snappymail/v/0.0.0/app/localization/ru-RU/admin.json +++ b/snappymail/v/0.0.0/app/localization/ru-RU/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Пользователь", "LABEL_STORAGE_PASSWORD": "Пароль", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Внимание!", "SUGGESTIONS_LIMIT": "Лимит предложений", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Не используйте этот тип базы данных с большим числом активных пользователей.", diff --git a/snappymail/v/0.0.0/app/localization/sk-SK/admin.json b/snappymail/v/0.0.0/app/localization/sk-SK/admin.json index a0afdb98b..ea6c2c109 100644 --- a/snappymail/v/0.0.0/app/localization/sk-SK/admin.json +++ b/snappymail/v/0.0.0/app/localization/sk-SK/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Užívateľ", "LABEL_STORAGE_PASSWORD": "Heslo", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Notice!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Nepoužívajte tento typ databázy, ak máte veľký počet aktívnych používateľov.", diff --git a/snappymail/v/0.0.0/app/localization/sl-SI/admin.json b/snappymail/v/0.0.0/app/localization/sl-SI/admin.json index 0a0ddfb1f..96a273441 100644 --- a/snappymail/v/0.0.0/app/localization/sl-SI/admin.json +++ b/snappymail/v/0.0.0/app/localization/sl-SI/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Ime podatkovnega vira (DSN)", "LABEL_STORAGE_USER": "Uporabnik", "LABEL_STORAGE_PASSWORD": "Geslo", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Obvestilo!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Ta tip podatkovne baze ni priporočljiv za večje število uporabnikov.", diff --git a/snappymail/v/0.0.0/app/localization/sv-SE/admin.json b/snappymail/v/0.0.0/app/localization/sv-SE/admin.json index 70e86c822..81f18360b 100644 --- a/snappymail/v/0.0.0/app/localization/sv-SE/admin.json +++ b/snappymail/v/0.0.0/app/localization/sv-SE/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Användare", "LABEL_STORAGE_PASSWORD": "Lösenord", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Notis", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Använd inte denna typ av databas med ett stort antal aktiva användare.", diff --git a/snappymail/v/0.0.0/app/localization/vi-VN/admin.json b/snappymail/v/0.0.0/app/localization/vi-VN/admin.json index a9a90a172..4ea995da8 100644 --- a/snappymail/v/0.0.0/app/localization/vi-VN/admin.json +++ b/snappymail/v/0.0.0/app/localization/vi-VN/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "Người dùng", "LABEL_STORAGE_PASSWORD": "Mật khẩu", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "Chú ý!", "SUGGESTIONS_LIMIT": "Suggestions limit", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Đừng nên sử dụng loại cơ sở dữ liệu này với lượng lớn người dùng mail đang hoạt động.", diff --git a/snappymail/v/0.0.0/app/localization/zh-CN/admin.json b/snappymail/v/0.0.0/app/localization/zh-CN/admin.json index e5807cf13..75172ffd1 100644 --- a/snappymail/v/0.0.0/app/localization/zh-CN/admin.json +++ b/snappymail/v/0.0.0/app/localization/zh-CN/admin.json @@ -68,6 +68,7 @@ "LABEL_STORAGE_DSN": "Dsn", "LABEL_STORAGE_USER": "用户", "LABEL_STORAGE_PASSWORD": "密码", + "SQLITE_GLOBAL": "Use a global DB instead of per account", "ALERT_NOTICE": "提示!", "SUGGESTIONS_LIMIT": "建议数量上限", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "如果有大量的活跃用户,请不要选择此数据库类型。", diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsContacts.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsContacts.html index d15b120d4..10d342652 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsContacts.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsContacts.html @@ -86,22 +86,23 @@ - -
- - - - - -
+ +
+
+

+
+
From b1c435277c7f6d0847783d014647c7ba7b32c9df Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 17 Jan 2024 01:34:25 +0100 Subject: [PATCH 007/540] Resolve #1393 --- plugins/avatars/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 06fb2e30e..41c19ee8c 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.13', - RELEASE = '2024-01-16', + VERSION = '1.14', + RELEASE = '2024-01-17', REQUIRED = '2.25.0', CATEGORY = 'Contacts', LICENSE = 'MIT', @@ -246,7 +246,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } if ($this->Config()->Get('plugin', 'gravatar', false)) { - $aUrls[] = 'http://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404'; + $aUrls[] = 'https://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404'; } foreach ($aUrls as $sUrl) { From 7cf5edc74a36d0d991aab83371b96a4c860a1ab4 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 17 Jan 2024 01:58:49 +0100 Subject: [PATCH 008/540] Alsp check global AddressBook.sqlite on ClearUserData() --- snappymail/v/0.0.0/app/libraries/RainLoop/Api.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 5b8e8796b..f1d3de2fc 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php @@ -111,9 +111,13 @@ abstract class Api $oStorageProvider->DeleteStorage($sEmail); } - $oAddressBookProvider = static::Actions()->AddressBookProvider(); - if ($oAddressBookProvider) { - $oAddressBookProvider->DeleteAllContacts($sEmail); + $oConfig = static::Config(); + $sqlite_global = $oConfig->Get('contacts', 'sqlite_global', false); + if ('sqlite' != $oConfig->Get('contacts', 'type', '') || \is_file(APP_PRIVATE_DATA . '/AddressBook.sqlite')) { + $oConfig->Set('contacts', 'sqlite_global', true); + $oAddressBookProvider = static::Actions()->AddressBookProvider(); + $oAddressBookProvider && $oAddressBookProvider->DeleteAllContacts($sEmail); + $oConfig->Set('contacts', 'sqlite_global', !!$sqlite_global); } return true; From ad985f94e56b7bb03233ad37d112e5e26495bb5d Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 17 Jan 2024 01:59:44 +0100 Subject: [PATCH 009/540] Change a weird translation. This should be better --- snappymail/v/0.0.0/app/localization/en/admin.json | 2 +- snappymail/v/0.0.0/app/localization/sk-SK/admin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/snappymail/v/0.0.0/app/localization/en/admin.json b/snappymail/v/0.0.0/app/localization/en/admin.json index 5032add7c..6b5475e47 100644 --- a/snappymail/v/0.0.0/app/localization/en/admin.json +++ b/snappymail/v/0.0.0/app/localization/en/admin.json @@ -79,7 +79,7 @@ "BUTTON_ADD_DOMAIN": "Add Domain", "BUTTON_ADD_ALIAS": "Add Alias", "DELETE_ARE_YOU_SURE": "Are you sure?", - "HTML_DOMAINS_HELPER": "List of domains webmail is allowed to access.\n
\nClick on the name to configure the domain.\n" + "HTML_DOMAINS_HELPER": "List of domains that are allowed.\n
\nClick on the name to configure the domain.\n" }, "TAB_SECURITY": { "LEGEND_SECURITY": "Security", diff --git a/snappymail/v/0.0.0/app/localization/sk-SK/admin.json b/snappymail/v/0.0.0/app/localization/sk-SK/admin.json index ea6c2c109..78f09a3c1 100644 --- a/snappymail/v/0.0.0/app/localization/sk-SK/admin.json +++ b/snappymail/v/0.0.0/app/localization/sk-SK/admin.json @@ -79,7 +79,7 @@ "BUTTON_ADD_DOMAIN": "Pridať doménu", "BUTTON_ADD_ALIAS": "Pridať alias", "DELETE_ARE_YOU_SURE": "Ste si istí?", - "HTML_DOMAINS_HELPER": "List of domains webmail is allowed to access.\n
\nClick on the name to configure the domain.\n" + "HTML_DOMAINS_HELPER": "List of domains that are allowed.\n
\nClick on the name to configure the domain.\n" }, "TAB_SECURITY": { "LEGEND_SECURITY": "Bezpečnosť", From 48c3d655b4971c6e6c14b76c4ce0b02ded599b03 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Thu, 18 Jan 2024 03:17:27 +0100 Subject: [PATCH 010/540] Merge code to generate MIME PGP parts and MIME Plain parts --- .../0.0.0/app/libraries/MailSo/Mime/Part.php | 34 ++++++++++ .../libraries/RainLoop/Actions/Messages.php | 68 ++----------------- 2 files changed, 38 insertions(+), 64 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php index b40864e4d..f03fd9ba8 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php @@ -182,4 +182,38 @@ class Part return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aSubStreams); } + + public function addPgpEncrypted(string $sEncrypted) + { + $oPart = new MimePart; + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/encrypted; protocol="application/pgp-encrypted"'); + $this->SubParts->append($oPart); + + $oSubPart = new MimePart; + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); + $oPart->SubParts->append($oSubPart); + + $oSubPart = new MimePart; + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sEncrypted); + $oPart->SubParts->append($oSubPart); + } + + public function addPlain(string $sPlain) + { + $oPart = new MimePart; + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset=utf-8'); + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); + $oPart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( + \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))), + 'convert.quoted-printable-encode' + ); + $this->SubParts->append($oPart); + } + } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 7eaf69a02..d34e9d0d8 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -906,8 +906,6 @@ trait Messages if ($this->Config()->Get('security', 'hide_x_mailer_header', true)) { $oMessage->DoesNotAddDefaultXMailer(); - } else { - $oMessage->SetXMailer('SnappyMail/'.APP_VERSION); } $oMessage->SetFrom(new \MailSo\Mime\Email($oIdentity->Email(), $oIdentity->Name())); @@ -952,8 +950,6 @@ trait Messages if ($this->Config()->Get('security', 'hide_x_mailer_header', true)) { $oMessage->DoesNotAddDefaultXMailer(); - } else { - $oMessage->SetXMailer('SnappyMail/'.APP_VERSION); } $sFrom = $this->GetActionParam('from', ''); @@ -1029,28 +1025,7 @@ trait Messages unset($sSigned); } else if ($sEncrypted = $this->GetActionParam('encrypted', '')) { - $oPart = new MimePart; - $oPart->Headers->AddByName( - MimeEnumHeader::CONTENT_TYPE, - 'multipart/encrypted; protocol="application/pgp-encrypted"' - ); - $oMessage->SubParts->append($oPart); - - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pgp-encrypted'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_DISPOSITION, 'attachment'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); - $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); - $oPart->SubParts->append($oAlternativePart); - - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/octet-stream'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); - $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sEncrypted))); - $oPart->SubParts->append($oAlternativePart); - - unset($oAlternativePart); + $oPart->addPgpEncrypted(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sEncrypted))); unset($sEncrypted); } else if ($sHtml = $this->GetActionParam('html', '')) { @@ -1064,14 +1039,7 @@ trait Messages // First add plain $sPlain = $this->GetActionParam('plain', '') ?: \MailSo\Base\HtmlUtils::ConvertHtmlToPlain($sHtml); $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sPlain)); - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/plain; charset=utf-8'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); - $oAlternativePart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( - \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))), - 'convert.quoted-printable-encode' - ); - $oPart->SubParts->append($oAlternativePart); + $oPart->addPlain($sPlain); unset($sPlain); // Now add HTML @@ -1114,15 +1082,7 @@ trait Messages } else { */ $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sPlain)); - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/plain; charset="utf-8"'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); - $oAlternativePart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( - \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))), - 'convert.quoted-printable-encode' - ); - $oMessage->SubParts->append($oAlternativePart); - unset($oAlternativePart); + $oMessage->addPlain($sPlain); unset($sPlain); } unset($oPart); @@ -1230,28 +1190,8 @@ trait Messages foreach ($aFingerprints as $sFingerprint) { $GPG->addEncryptKey($sFingerprint); } - $sEncrypted = $GPG->encryptStream($fp); - $oPart = new MimePart; - $oPart->Headers->AddByName( - MimeEnumHeader::CONTENT_TYPE, - 'multipart/encrypted; protocol="application/pgp-encrypted"' - ); - $oMessage->SubParts->append($oPart); - - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pgp-encrypted'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_DISPOSITION, 'attachment'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); - $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); - $oPart->SubParts->append($oAlternativePart); - - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/octet-stream'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); - $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); - $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sEncrypted); - $oPart->SubParts->append($oAlternativePart); + $oPart->addPgpEncrypted($GPG->encryptStream($fp)); } $this->Plugins()->RunHook('filter.build-message', array($oMessage)); From 55a5fb491a5c83c454f0a676b0f0958c437cbf1b Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Thu, 18 Jan 2024 03:19:03 +0100 Subject: [PATCH 011/540] Change SMTP sendRequestWithCheck for future support of RFC's --- .../app/libraries/MailSo/Smtp/SmtpClient.php | 126 +++++++++++++----- 1 file changed, 94 insertions(+), 32 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php index b1ebe5010..0e3261cbb 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/SmtpClient.php @@ -7,6 +7,9 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. + * + * https://datatracker.ietf.org/doc/html/rfc2034 + * https://datatracker.ietf.org/doc/html/rfc2821 */ namespace MailSo\Smtp; @@ -149,7 +152,7 @@ class SmtpClient extends \MailSo\Net\NetClient // https://github.com/the-djmaze/snappymail/issues/1038 try { - $sResult = $this->sendRequestWithCheck('AUTH', 235, 'PLAIN ' . $SASL->authenticate($sLogin, $sPassword)); + $sResult = $this->sendRequestWithCheck('AUTH PLAIN ' . $SASL->authenticate($sLogin, $sPassword), 235); } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) { @@ -161,7 +164,7 @@ class SmtpClient extends \MailSo\Net\NetClient // Start authentication try { - $sResult = $this->sendRequestWithCheck('AUTH', 334, $type); + $sResult = $this->sendRequestWithCheck("AUTH {$type}", 334); } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) { @@ -174,10 +177,10 @@ class SmtpClient extends \MailSo\Net\NetClient { if (\str_starts_with($type, 'SCRAM-')) { // RFC 5802 - $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 234, ''); + $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 234); $sChallenge = $SASL->challenge($sResult); $this->logMask($sChallenge); - $SASL->verify($this->sendRequestWithCheck($sChallenge, 235, '', true)); + $SASL->verify($this->sendRequestWithCheck($sChallenge, 235, true)); } else switch ($type) { // RFC 4616 case 'PLAIN': @@ -185,14 +188,14 @@ class SmtpClient extends \MailSo\Net\NetClient case 'OAUTHBEARER': $sAuth = $SASL->authenticate($sLogin, $sPassword); $this->logMask($sAuth); - $this->sendRequestWithCheck($sAuth, 235, '', true); + $this->sendRequestWithCheck($sAuth, 235, true); break; case 'LOGIN': - $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 334, ''); + $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 334); $sPassword = $SASL->challenge($sResult); $this->logMask($sPassword); - $this->sendRequestWithCheck($sPassword, 235, '', true); + $this->sendRequestWithCheck($sPassword, 235, true); break; // RFC 2195 @@ -200,7 +203,7 @@ class SmtpClient extends \MailSo\Net\NetClient if (empty($sResult)) { $this->writeLogException(new \MailSo\Smtp\Exceptions\NegativeResponseException); } - $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, '', true); + $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, true); break; } } @@ -240,6 +243,18 @@ class SmtpClient extends \MailSo\Net\NetClient // RFC 6152 if ($this->hasCapability('8BITMIME')) { // $sCmd .= ' BODY=8BITMIME'; + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { +// $sCmd .= ' SMTPUTF8'; + } + } + // RFC 3030 + else if ($this->hasCapability('BINARYMIME')) { +// $sCmd .= ' BODY=BINARYMIME'; + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { +// $sCmd .= ' SMTPUTF8'; + } } // RFC 8689 @@ -247,7 +262,7 @@ class SmtpClient extends \MailSo\Net\NetClient $sCmd .= ' REQUIRETLS'; } - $this->sendRequestWithCheck('MAIL', 250, $sCmd); + $this->sendRequestWithCheck("MAIL {$sCmd}", 250); $this->bMail = true; $this->bRcpt = false; @@ -274,10 +289,7 @@ class SmtpClient extends \MailSo\Net\NetClient $sCmd .= ' NOTIFY=SUCCESS,FAILURE'; } - $this->sendRequestWithCheck( - 'RCPT', array(250, 251), $sCmd, false, - 'Failed to add recipient "'.$sTo.'"' - ); + $this->sendRequestWithCheck("RCPT {$sCmd}", array(250, 251), false, "Failed to add recipient '{$sTo}'"); $this->bRcpt = true; @@ -331,6 +343,25 @@ class SmtpClient extends \MailSo\Net\NetClient $this->writeLogException(new \MailSo\RuntimeException('No recipient forward path has been supplied'), \LOG_ERR); } +/* + // RFC 3030 + if ($this->hasCapability('CHUNKING')) { + $this->bRunningCallback = true; + while (!\feof($rDataStream)) { + $sBuffer = \fgets($rDataStream); + if (false === $sBuffer) { + if (!\feof($rDataStream)) { + $this->writeLogException(new \MailSo\RuntimeException('Cannot read input resource'), \LOG_ERR); + } + break; + } + $this->sendRequestWithCheck("BDAT " . \strlen($sBuffer) . "\r\n{$sBuffer}", 250); + \MailSo\Base\Utils::ResetTimeLimit(); + } + $this->sendRequestWithCheck("BDAT 0 LAST\r\n", 250); + } + else { +*/ $this->sendRequestWithCheck('DATA', 354); $this->writeLog('Message data.'); @@ -375,17 +406,58 @@ class SmtpClient extends \MailSo\Net\NetClient } /** + * VERIFY * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Smtp\Exceptions\* */ public function Vrfy(string $sUser) : self { - $this->sendRequestWithCheck('VRFY', array(250, 251, 252), - \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser))); +/* + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { + $this->sendRequestWithCheck('VRFY ' + . \MailSo\Base\Utils::IdnToUtf8(\MailSo\Base\Utils::Trim($sUser)) + . ' SMTPUTF8', + array(250, 251, 252), + ); + } else { +*/ + $this->sendRequestWithCheck( + 'VRFY ' . \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser)), + array(250, 251, 252) + ); return $this; } + /** + * EXPAND command, the string identifies a mailing list, and the + * successful (i.e., 250) multiline response MAY include the full name + * of the users and MUST give the mailboxes on the mailing list. + * + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Smtp\Exceptions\* + */ +/* + public function Expn(string $sUser) : self + { + // RFC 6531 + if ($this->hasCapability('SMTPUTF8')) { + $this->sendRequestWithCheck('EXPN ' + . \MailSo\Base\Utils::IdnToUtf8(\MailSo\Base\Utils::Trim($sUser)) + . ' SMTPUTF8', + array(250, 251, 252) + ); + } else { + $this->sendRequestWithCheck('EXPN ' + . \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser)), + array(250, 251, 252), + ); + return $this; + } +*/ + /** * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* @@ -394,7 +466,6 @@ class SmtpClient extends \MailSo\Net\NetClient public function Noop() : self { $this->sendRequestWithCheck('NOOP', 250); - return $this; } @@ -417,20 +488,15 @@ class SmtpClient extends \MailSo\Net\NetClient * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* */ - private function sendRequest(string $sCommand, string $sAddToCommand = '', bool $bSecureLog = false) : void + private function sendRequest(string $sCommand, bool $bSecureLog = false) : void { - $sCommand = \trim($sCommand); - if (!\strlen($sCommand)) { + if (!\strlen(\trim($sCommand))) { $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } $this->IsConnected(true); - $sRealCommand = $sCommand . (\strlen($sAddToCommand) ? ' '.$sAddToCommand : ''); - - $sFakeCommand = $bSecureLog ? '********' : ''; - - $this->sendRaw($sRealCommand, true, $sFakeCommand); + $this->sendRaw($sCommand, true, $bSecureLog ? '********' : ''); } /** @@ -439,9 +505,9 @@ class SmtpClient extends \MailSo\Net\NetClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Smtp\Exceptions\* */ - private function sendRequestWithCheck(string $sCommand, $mExpectCode, string $sAddToCommand = '', bool $bSecureLog = false, string $sErrorPrefix = '') : string + private function sendRequestWithCheck(string $sCommand, $mExpectCode, bool $bSecureLog = false, string $sErrorPrefix = '') : string { - $this->sendRequest($sCommand, $sAddToCommand, $bSecureLog); + $this->sendRequest($sCommand, $bSecureLog); $this->validateResponse($mExpectCode, $sErrorPrefix); return empty($this->aResults[0]) ? '' : \trim(\substr($this->aResults[0], 4)); } @@ -472,16 +538,12 @@ class SmtpClient extends \MailSo\Net\NetClient */ private function ehlo(string $sHost) : void { - $this->sendRequestWithCheck('EHLO', 250, $sHost); + $this->sendRequestWithCheck("EHLO {$sHost}", 250); /* 250-PIPELINING\r\n 250-SIZE 256000000\r\n 250-ETRN\r\n - 250-STARTTLS\r\n 250-ENHANCEDSTATUSCODES\r\n - 250-8BITMIME\r\n - 250-DSN\r\n - 250 SMTPUTF8\r\n */ $this->aCapa = []; foreach ($this->aResults as $sLine) { @@ -513,7 +575,7 @@ class SmtpClient extends \MailSo\Net\NetClient */ private function helo(string $sHost) : void { - $this->sendRequestWithCheck('HELO', 250, $sHost); + $this->sendRequestWithCheck("HELO {$sHost}", 250); $this->aAuthTypes = array(); $this->iSizeCapaValue = 0; $this->aCapa = []; From 8292c47f7c9d6d7d8530c6e37d03d35ee688362f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 21 Jan 2024 23:35:32 +0100 Subject: [PATCH 012/540] Return all fetched messages headers in JSON. This is in preperation of BIMI-Selectors #1394 --- dev/Model/Message.js | 38 ++++++++++++++ dev/Model/MimeHeader.js | 13 +++++ dev/Model/MimeHeaderCollection.js | 32 ++++++++++++ .../app/libraries/MailSo/Mail/MailClient.php | 8 +-- .../app/libraries/MailSo/Mail/Message.php | 52 ++++--------------- .../app/libraries/MailSo/Mime/Header.php | 43 +++++++++------ .../MailSo/Mime/HeaderCollection.php | 18 ++++--- 7 files changed, 135 insertions(+), 69 deletions(-) create mode 100644 dev/Model/MimeHeader.js create mode 100644 dev/Model/MimeHeaderCollection.js diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 8a85626f5..000635e34 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -14,6 +14,7 @@ import { SettingsUserStore } from 'Stores/User/Settings'; import { FileInfo, RFC822 } from 'Common/File'; import { AttachmentCollectionModel } from 'Model/AttachmentCollection'; import { EmailCollectionModel } from 'Model/EmailCollection'; +import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection'; import { AbstractModel } from 'Knoin/AbstractModel'; import PreviewHTML from 'Html/PreviewMessage.html'; @@ -122,6 +123,7 @@ export class MessageModel extends AbstractModel { this.threadUnseen = ko.observableArray(); this.unsubsribeLinks = ko.observableArray(); this.flags = ko.observableArray(); + this.headers = ko.observableArray(new MimeHeaderCollectionModel); addComputablesTo(this, { attachmentIconClass: () => @@ -224,8 +226,44 @@ export class MessageModel extends AbstractModel { if (super.revivePropertiesFromJson(json)) { // this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : []; // this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs)); +// this.headers(MimeHeaderCollectionModel.reviveFromJson(json.headers)); this.computeSenderEmail(); + + let value, headers = this.headers(); +/* // These could be by Envelope or MIME + this.messageId = headers.valueByName('Message-Id'); + this.subject(headers.valueByName('Subject')); + this.sender = EmailCollectionModel.fromString(headers.valueByName('Sender')); + this.from = EmailCollectionModel.fromString(headers.valueByName('From')); + this.replyTo = EmailCollectionModel.fromString(headers.valueByName('Reply-To')); + this.to = EmailCollectionModel.fromString(headers.valueByName('To')); + this.cc = EmailCollectionModel.fromString(headers.valueByName('Cc')); + this.bcc = EmailCollectionModel.fromString(headers.valueByName('Bcc')); + this.inReplyTo = headers.valueByName('In-Reply-To'); + + this.deliveredTo = EmailCollectionModel.fromString(headers.valueByName('Delivered-To')); +*/ + // Priority + value = headers.valueByName('X-MSMail-Priority') + || headers.valueByName('Importance') + || headers.valueByName('X-Priority'); + if (value) { + if (/[h12]/.test(value[0])) { + this.priority(1); + } else if (/[l45]/.test(value[0])) { + this.priority(5); + } + } + + // Unsubscribe links + value = headers.valueByName('List-Unsubscribe'); + if (value) { + this.unsubsribeLinks(value.split(',').map( + link => link.replace(/^[ <>]+|[ <>]+$/g, '') + )); + } + return true; } } diff --git a/dev/Model/MimeHeader.js b/dev/Model/MimeHeader.js new file mode 100644 index 000000000..5b4de7f7f --- /dev/null +++ b/dev/Model/MimeHeader.js @@ -0,0 +1,13 @@ +import ko from 'ko'; + +import { AbstractModel } from 'Knoin/AbstractModel'; + +export class MimeHeaderModel extends AbstractModel +{ + constructor() { + super(); + this.name = ''; + this.value = ''; + this.parameters = ko.observableArray(); + } +} diff --git a/dev/Model/MimeHeaderCollection.js b/dev/Model/MimeHeaderCollection.js new file mode 100644 index 000000000..6240e1285 --- /dev/null +++ b/dev/Model/MimeHeaderCollection.js @@ -0,0 +1,32 @@ +import { AbstractCollectionModel } from 'Model/AbstractCollection'; +import { MimeHeaderModel } from 'Model/MimeHeader'; + +'use strict'; + +export class MimeHeaderCollectionModel extends AbstractCollectionModel +{ + /** + * @param {?Array} json + * @returns {MimeHeaderCollectionModel} + */ + static reviveFromJson(items) { + return super.reviveFromJson(items, header => MimeHeaderModel.reviveFromJson(header)); + } + + /** + * @param {string} name + * @returns {?MimeHeader} + */ + getByName(name) + { + name = name.toLowerCase(); + return this.find(header => header.name.toLowerCase() === name); + } + + valueByName(name) + { + const header = this.getByName(name); + return header ? header.value : ''; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index fc4f9f20f..a81a50a3a 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -47,9 +47,9 @@ class MailClient } return FetchType::BuildBodyCustomHeaderRequest(array( - MimeHeader::RETURN_PATH, - MimeHeader::RECEIVED, - MimeHeader::MIME_VERSION, +// MimeHeader::RETURN_PATH, +// MimeHeader::RECEIVED, +// MimeHeader::MIME_VERSION, MimeHeader::MESSAGE_ID, MimeHeader::CONTENT_TYPE, MimeHeader::FROM_, @@ -67,7 +67,7 @@ class MailClient MimeHeader::IMPORTANCE, MimeHeader::X_PRIORITY, MimeHeader::X_DRAFT_INFO, - MimeHeader::RETURN_RECEIPT_TO, +// MimeHeader::RETURN_RECEIPT_TO, MimeHeader::DISPOSITION_NOTIFICATION_TO, MimeHeader::X_CONFIRM_READING_TO, MimeHeader::AUTHENTICATION_RESULTS, diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index e3a950a5c..6e470f098 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -32,7 +32,6 @@ class Message implements \JsonSerializable $sPlain = '', $sHtml = '', $References = '', - $sDeliveryReceipt = '', $ReadReceipt = ''; private ?string @@ -53,8 +52,7 @@ class Message implements \JsonSerializable $iSize = 0, $SpamScore = 0, $iInternalTimeStampInUTC = 0, - $iHeaderTimeStampInUTC = 0, - $iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL; + $iHeaderTimeStampInUTC = 0; private bool $bIsSpam = false; @@ -73,7 +71,6 @@ class Message implements \JsonSerializable $DMARC = [], // $aFlags = [], $aFlagsLowerCase = [], - $UnsubsribeLinks = [], $aThreadUIDs = [], $aThreadUnseenUIDs = []; @@ -93,6 +90,9 @@ class Message implements \JsonSerializable private ?AttachmentCollection $Attachments = null; + private ?\MailSo\Mime\HeaderCollection + $Headers = null; + function __get($k) { return \property_exists($this, $k) ? $this->$k : null; @@ -164,6 +164,8 @@ class Message implements \JsonSerializable $sHeaders = $oFetchResponse->GetHeaderFieldsValue(); $oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null; if ($oHeaders) { + $oMessage->Headers = $oHeaders; + $sContentTypeCharset = $oHeaders->ParameterValue( MimeHeader::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::CHARSET @@ -200,29 +202,8 @@ class Message implements \JsonSerializable $oHeaders->ValueByName(MimeHeader::DATE) ); - // Priority - $sPriority = $oHeaders->ValueByName(MimeHeader::X_MSMAIL_PRIORITY) - ?: $oHeaders->ValueByName(MimeHeader::IMPORTANCE) - ?: $oHeaders->ValueByName(MimeHeader::X_PRIORITY); - if (\strlen($sPriority)) { - switch (\substr(\trim($sPriority), 0, 1)) - { - case 'h': - case '1': - case '2': - $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH; - break; - - case 'l': - case '4': - case '5': - $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW; - break; - } - } - // Delivery Receipt - $oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(MimeHeader::RETURN_RECEIPT_TO)); +// $oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(MimeHeader::RETURN_RECEIPT_TO)); // Read Receipt $oMessage->ReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::DISPOSITION_NOTIFICATION_TO)); @@ -230,17 +211,6 @@ class Message implements \JsonSerializable $oMessage->ReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::X_CONFIRM_READING_TO)); } - // Unsubscribe links - $UnsubsribeLinks = $oHeaders->ValueByName(MimeHeader::LIST_UNSUBSCRIBE); - if ($UnsubsribeLinks) { - $oMessage->UnsubsribeLinks = \array_map( - function ($link) { - return trim($link, ' <>'); - }, - \explode(',', $UnsubsribeLinks) - ); - } - if ($spam = $oHeaders->ValueByName(MimeHeader::X_SPAMD_RESULT)) { if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) { if ($threshold = \floatval($match[2])) { @@ -554,7 +524,6 @@ class Message implements \JsonSerializable 'sender' => $this->oSender, 'deliveredTo' => $this->oDeliveredTo, - 'priority' => $this->iPriority, 'readReceipt' => $sReadReceipt, 'autocrypt' => $aAutocrypt ?: null, @@ -576,15 +545,14 @@ class Message implements \JsonSerializable // 'keywords' => $keywords, 'size' => $this->iSize, - 'preview' => $this->sPreview + 'preview' => $this->sPreview, + + 'headers' => $this->Headers ); if ($this->DraftInfo) { $result['draftInfo'] = $this->DraftInfo; } - if ($this->UnsubsribeLinks) { - $result['unsubsribeLinks'] = $this->UnsubsribeLinks; - } if ($this->References) { $result['references'] = $this->References; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php index 65f890dbc..eff1a3275 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php @@ -15,7 +15,7 @@ namespace MailSo\Mime; * @category MailSo * @package Mime */ -class Header +class Header implements \JsonSerializable { private string $sName; @@ -41,7 +41,7 @@ class Header $this->sFullValue = \trim($sValue); $this->sEncodedValueForReparse = ''; - if (\strlen($sEncodedValueForReparse) && $this->IsReparsed()) { + if (\strlen($sEncodedValueForReparse) && ($this->IsEmail() || $this->IsSubject() || $this->IsParameterized())) { $this->sEncodedValueForReparse = \trim($sEncodedValueForReparse); } @@ -88,7 +88,7 @@ class Header public function NameWithDelimitrom() : string { - return $this->Name().': '; + return $this->sName . ': '; } public function Value() : string @@ -103,7 +103,7 @@ class Header public function SetParentCharset(string $sParentCharset) : Header { - if ($this->sParentCharset !== $sParentCharset && $this->IsReparsed() && \strlen($this->sEncodedValueForReparse)) { + if ($this->sParentCharset !== $sParentCharset && \strlen($this->sEncodedValueForReparse)) { $this->initInputData( $this->sName, \trim(\MailSo\Base\Utils::DecodeHeaderValue($this->sEncodedValueForReparse, $sParentCharset)), @@ -132,19 +132,17 @@ class Header if ($this->IsSubject()) { if (!\MailSo\Base\Utils::IsAscii($sResult) && \function_exists('iconv_mime_encode')) { - $aPreferences = array( + return \iconv_mime_encode($this->Name(), $sResult, array( // 'scheme' => \MailSo\Base\Enumerations\Encoding::QUOTED_PRINTABLE_SHORT, 'scheme' => \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, 'input-charset' => \MailSo\Base\Enumerations\Charset::UTF_8, 'output-charset' => \MailSo\Base\Enumerations\Charset::UTF_8, 'line-length' => 74, 'line-break-chars' => "\r\n" - ); - - return \iconv_mime_encode($this->Name(), $sResult, $aPreferences); + )); } } - else if ($this->IsParameterized() && 0 < $this->oParameters->count()) + else if ($this->IsParameterized() && $this->oParameters->count()) { $sResult = $this->sValue.'; '.$this->oParameters->ToString(true); } @@ -157,16 +155,15 @@ class Header } // https://www.rfc-editor.org/rfc/rfc2822#section-2.1.1, avoid folding immediately after the header name - return $this->NameWithDelimitrom() . \wordwrap($sResult, 78 - \strlen($this->NameWithDelimitrom()) - 1, "\r\n "); - + return $this->NameWithDelimitrom() . \wordwrap($sResult, 78 - \strlen($this->NameWithDelimitrom()) - 1, "\r\n "); } - public function IsSubject() : bool + private function IsSubject() : bool { return \strtolower(Enumerations\Header::SUBJECT) === \strtolower($this->Name()); } - public function IsParameterized() : bool + private function IsParameterized() : bool { return \in_array(\strtolower($this->sName), array( \strtolower(Enumerations\Header::CONTENT_TYPE), @@ -174,7 +171,7 @@ class Header )); } - public function IsEmail() : bool + private function IsEmail() : bool { return \in_array(\strtolower($this->sName), array( \strtolower(Enumerations\Header::FROM_), @@ -182,7 +179,7 @@ class Header \strtolower(Enumerations\Header::CC), \strtolower(Enumerations\Header::BCC), \strtolower(Enumerations\Header::REPLY_TO), - \strtolower(Enumerations\Header::RETURN_PATH), +// \strtolower(Enumerations\Header::RETURN_PATH), \strtolower(Enumerations\Header::SENDER) )); } @@ -199,8 +196,22 @@ class Header return $this->Value(); } - public function IsReparsed() : bool + private function IsReparsed() : bool { return $this->IsEmail() || $this->IsSubject() || $this->IsParameterized(); } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = array( + '@Object' => 'Object/MimeHeader', + 'name' => $this->sName, + 'value' => $this->sValue + ); + if ($this->oParameters->count()) { + $aResult['parameters'] = $this->oParameters; + } + return $aResult; + } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php index 4697cfd23..8d698dafd 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php @@ -81,15 +81,10 @@ class HeaderCollection extends \MailSo\Base\Collection ); } - public function ParametersByName(string $sHeaderName) : ?ParameterCollection - { - $oHeader = $this->GetByName($sHeaderName); - return $oHeader ? $oHeader->Parameters() : null; - } - public function ParameterValue(string $sHeaderName, string $sParamName) : string { - $oParameters = $this->ParametersByName($sHeaderName); + $oHeader = $this->GetByName($sHeaderName); + $oParameters = $oHeader ? $oHeader->Parameters() : null; return (null !== $oParameters) ? $oParameters->ParameterValueByName($sParamName) : ''; } @@ -244,4 +239,13 @@ class HeaderCollection extends \MailSo\Base\Collection { return \implode("\r\n", $this->getArrayCopy()); } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + '@Object' => 'Collection/MimeHeaderCollection', + '@Collection' => $this->getArrayCopy() + ); + } } From 58955f792a8a0681c4b317b2ea3b8b12fa6ed7f4 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 00:01:46 +0100 Subject: [PATCH 013/540] Changes for BIMI-Selector #1394 --- plugins/avatars/avatars.js | 9 ++++++++- plugins/avatars/index.php | 11 +++++++---- .../0.0.0/app/libraries/MailSo/Mail/MailClient.php | 2 ++ .../libraries/MailSo/Mime/Enumerations/Header.php | 3 +++ snappymail/v/0.0.0/app/libraries/snappymail/dns.php | 13 ++++++++----- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index 4fd61a924..d9f07de74 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -39,9 +39,15 @@ avatars = new Map, ncAvatars = new Map, templateId = 'MailMessageView', + getBimiSelector = msg => { + // Get 's' value out of 'v=BIMI1; s=foo;' + let bimiSelector = msg.headers().valueByName('BIMI-Selector'); + bimiSelector = bimiSelector ? bimiSelector.match(/;.*s=([^\s;]+)/)[1] : ''; + return bimiSelector || ''; + }, getAvatarUid = msg => { let from = msg.from[0], - bimi = 'pass' == from.dkimStatus ? 1 : 0; + bimi = ('pass' == from.dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg); return `${bimi}/${from.email.toLowerCase()}`; }, getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)), @@ -97,6 +103,7 @@ runQueue(); }, 'Avatar', { bimi: 'pass' == from.dkimStatus ? 1 : 0, + bimiSelector: getBimiSelector(item[0]), email: from.email }); break; diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 41c19ee8c..5e9854a16 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -84,8 +84,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin public function DoAvatar() : array { $bBimi = !empty($this->jsonParam('bimi')); + $sBimiSelector = $this->jsonParam('bimiSelector') ?: ''; $sEmail = $this->jsonParam('email'); - $aResult = $this->getAvatar($sEmail, !empty($bBimi)); + $aResult = $this->getAvatar($sEmail, $bBimi, $sBimiSelector); if ($aResult) { $aResult = [ 'type' => $aResult[0], @@ -103,7 +104,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail) { $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEmail); - if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($sBimi)))) { + $aBimi = \explode('-', $sBimi, 2); + $sBimiSelector = isset($aBimi[1]) ? $aBimi[1] : 'default'; + if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($aBimi[0]), $sBimiSelector))) { \header('Content-Type: '.$aResult[0]); echo $aResult[1]; } else { @@ -190,7 +193,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin return null; } - private function getAvatar(string $sEmail, bool $bBimi) : ?array + private function getAvatar(string $sEmail, bool $bBimi, string $sBimiSelector = '') : ?array { if (!\strpos($sEmail, '@')) { return null; @@ -235,7 +238,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $aUrls = []; if ($this->Config()->Get('plugin', 'bimi', false)) { - $BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain) : null; + $BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain, $sBimiSelector) : null; if ($BIMI) { $aUrls[] = $BIMI; // $aResult = ['text/uri-list', $BIMI]; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index a81a50a3a..994ed4feb 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -75,6 +75,8 @@ class MailClient MimeHeader::LIST_UNSUBSCRIBE, // https://autocrypt.org/level1.html#the-autocrypt-header MimeHeader::AUTOCRYPT, + // https://www.ietf.org/archive/id/draft-brand-indicators-for-message-identification-04.html#bimi-selector + MimeHeader::BIMI_SELECTOR, // SPAM MimeHeader::X_SPAM_STATUS, MimeHeader::X_SPAM_FLAG, diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/Header.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/Header.php index 53c74daeb..a65a25ff4 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/Header.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/Header.php @@ -93,5 +93,8 @@ abstract class Header // https://autocrypt.org/level1.html#the-autocrypt-header AUTOCRYPT = 'Autocrypt', + // https://www.ietf.org/archive/id/draft-brand-indicators-for-message-identification-04.html#bimi-selector + BIMI_SELECTOR = 'BIMI-Selector', + LIST_UNSUBSCRIBE = 'List-Unsubscribe'; } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/dns.php b/snappymail/v/0.0.0/app/libraries/snappymail/dns.php index cb3ebda20..c48f4f1c0 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/dns.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/dns.php @@ -6,12 +6,15 @@ abstract class DNS { /** * $domain = 'bimigroup.org' + * $selector = 'default' * Then a TXT lookup is done on 'default._bimi.bimigroup.org' */ - public static function BIMI(string $domain) : string + public static function BIMI(string $domain, string $selector = 'default') : string { + $selector = \trim($selector) ?: 'default'; $oCache = \RainLoop\Api::Actions()->Cacher(); - $BIMI = $oCache->Get("dns-bimi-{$domain}") ?: null; + $sCacheKey = "dns-bimi-{$domain}-{$selector}"; + $BIMI = $oCache->Get($sCacheKey) ?: null; if ($BIMI) { $BIMI = \json_decode($BIMI); if ($BIMI[1] < \time()) { @@ -22,12 +25,12 @@ abstract class DNS } if (null === $BIMI) { $BIMI = ''; - $values = \dns_get_record("default._bimi.{$domain}", \DNS_TXT); + $values = \dns_get_record("{$selector}._bimi.{$domain}", \DNS_TXT); if ($values) { foreach ($values as $value) { if (\str_starts_with($value['txt'], 'v=BIMI1')) { $BIMI = \preg_replace('/^.+l=([^;]+)(;.*)?$/D', '$1', $value['txt']); - $oCache->Set("dns-bimi-{$domain}", \json_encode([ + $oCache->Set($sCacheKey, \json_encode([ $BIMI, time() + $value['ttl'] ])); @@ -37,7 +40,7 @@ abstract class DNS } if (!$BIMI) { // Don't lookup for 24 hours - $oCache->Set("dns-bimi-{$domain}", \json_encode([ + $oCache->Set($sCacheKey, \json_encode([ $BIMI, time() + 86400 ])); From 5747b7add6de262569092294b43441da2a2ff4bb Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 00:05:06 +0100 Subject: [PATCH 014/540] Version bump for #1400 --- plugins/proxy-auth/index.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/proxy-auth/index.php b/plugins/proxy-auth/index.php index a69dd23de..5a6fc1227 100644 --- a/plugins/proxy-auth/index.php +++ b/plugins/proxy-auth/index.php @@ -6,8 +6,8 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'Proxy Auth', AUTHOR = 'Philipp', URL = 'https://www.mundhenk.org/', - VERSION = '0.1', - RELEASE = '2023-01-14', + VERSION = '0.2', + RELEASE = '2024-01-22', REQUIRED = '2.27.0', CATEGORY = 'Login', LICENSE = 'MIT', @@ -49,10 +49,10 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $sLevel = LOG_DEBUG; $sMsg = "sEmail= " . $sEmail; $oLogger->Write($sMsg, $sLevel, $sPrefix); - + $sMasterUser = \trim($this->Config()->getDecrypted('plugin', 'master_user', '')); $sMasterSeparator = \trim($this->Config()->getDecrypted('plugin', 'master_separator', '')); - + /* remove superuser from email for proper UI */ if (static::$login) { $sEmail = str_replace($sMasterUser, "", $sEmail); @@ -86,7 +86,7 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $sProxyCheck = $this->Config()->getDecrypted('plugin', 'proxy_check', ''); $sClientIPs = $this->Manager()->Actions()->Http()->GetClientIP(true); - + /* make sure that remote user is only set by authorized proxy to avoid security risks */ if ($sProxyCheck) { $sProxyRequest = false; @@ -103,7 +103,7 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $sProxyRequest = true; } } - } else { + } else { $sMsg = "checking client IP: " . $sClientIPs; $oLogger->Write($sMsg, $sLevel, $sPrefix); @@ -114,7 +114,7 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin } else { $sProxyRequest = true; } - + if ($sProxyRequest) { /* create master user login from remote user header and settings */ $sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser; From 4d9fb41d73dcc8698f7c94729809ba638e9777fc Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 09:03:34 +0100 Subject: [PATCH 015/540] Cleanup mime header parameter handling --- .../app/libraries/MailSo/Mime/Parameter.php | 29 +++++-------------- .../MailSo/Mime/ParameterCollection.php | 2 +- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php index 2e20ffe09..7a1bddc2f 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php @@ -27,18 +27,17 @@ class Parameter implements \JsonSerializable $this->sValue = $sValue; } - public static function CreateFromParameterLine(string $sRawParam) : self + public static function FromString(string $sRawParam) : self { $oParameter = new self('', ''); - return $oParameter->Parse($sRawParam); - } - public function Reset() : self - { - $this->sName = ''; - $this->sValue = ''; + $aParts = \explode('=', $sRawParam, 2); + $oParameter->sName = \trim(\trim($aParts[0]), '"\''); + if (2 === \count($aParts)) { + $oParameter->sValue = \trim(\trim($aParts[1]), '"\''); + } - return $this; + return $oParameter; } public function Name() : string @@ -56,20 +55,6 @@ class Parameter implements \JsonSerializable $this->sValue = $sValue; } - public function Parse(string $sRawParam, string $sSeparator = '=') : self - { - $this->Reset(); - - $aParts = \explode($sSeparator, $sRawParam, 2); - - $this->sName = \trim(\trim($aParts[0]), '"\''); - if (2 === \count($aParts)) { - $this->sValue = \trim(\trim($aParts[1]), '"\''); - } - - return $this; - } - public function ToString(bool $bConvertSpecialsName = false) : string { if (!\strlen($this->sName)) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php index af34acb40..ee898be01 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/ParameterCollection.php @@ -64,7 +64,7 @@ class ParameterCollection extends \MailSo\Base\Collection $aDataToParse = \explode(';', $sRawParams); foreach ($aDataToParse as $sParam) { - $this->append(Parameter::CreateFromParameterLine($sParam)); + $this->append(Parameter::FromString($sParam)); } $this->reParseParameters(); From 8749d189df9d7b4372e50eb07560a96147b579d0 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 09:19:45 +0100 Subject: [PATCH 016/540] cleanup mime header encoding handling --- .../app/libraries/MailSo/Base/Enumerations/Encoding.php | 1 - snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php | 5 ++++- snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php | 2 +- snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php | 4 +--- snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php | 4 +--- .../v/0.0.0/app/libraries/RainLoop/Actions/Messages.php | 3 +-- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Encoding.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Encoding.php index ad9dc20b8..017744eab 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Encoding.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Enumerations/Encoding.php @@ -21,7 +21,6 @@ abstract class Encoding const QUOTED_PRINTABLE_LOWER = 'quoted-printable'; const QUOTED_PRINTABLE_SHORT = 'Q'; - const BASE64 = 'Base64'; const BASE64_LOWER = 'base64'; const BASE64_SHORT = 'B'; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php index be1da5ff9..3d8793147 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php @@ -356,7 +356,10 @@ abstract class Utils return $sResultHeaders; } - public static function EncodeUnencodedValue(string $sEncodeType, string $sValue) : string + /** + * https://datatracker.ietf.org/doc/html/rfc2047 + */ + public static function EncodeHeaderValue(string $sValue, string $sEncodeType = 'B') : string { $sValue = \trim($sValue); if (\strlen($sValue) && !static::IsAscii($sValue)) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php index 904764c80..1fb069096 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Attachment.php @@ -153,7 +153,7 @@ class Attachment $oAttachmentPart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream($oAttachmentPart->Body, \MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName( - \MailSo\Base\Enumerations\Encoding::BASE64, false)); + \MailSo\Base\Enumerations\Encoding::BASE64_LOWER, false)); \MailSo\Base\StreamWrappers\Binary::RememberStream($oAttachmentPart->Body); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php index 46decf217..88559eff7 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php @@ -184,9 +184,7 @@ class Email implements \JsonSerializable $sDisplayName = \str_replace('"', '\"', $this->sDisplayName); if ($bConvertSpecialsName) { - $sDisplayName = \strlen($sDisplayName) - ? \MailSo\Base\Utils::EncodeUnencodedValue(\MailSo\Base\Enumerations\Encoding::BASE64_SHORT, $sDisplayName) - : ''; + $sDisplayName = \strlen($sDisplayName) ? \MailSo\Base\Utils::EncodeHeaderValue($sDisplayName) : ''; } $sDisplayName = \strlen($sDisplayName) ? '"'.$sDisplayName.'"' : ''; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php index 7a1bddc2f..e73ce1df5 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parameter.php @@ -66,9 +66,7 @@ class Parameter implements \JsonSerializable \strtolower(Enumerations\Parameter::FILENAME) ))) { - return $this->sName . '="' . \MailSo\Base\Utils::EncodeUnencodedValue( - \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, - $this->sValue) . '"'; + return $this->sName . '="' . \MailSo\Base\Utils::EncodeHeaderValue($this->sValue) . '"'; } return $this->sName . '="' . $this->sValue . '"'; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index d34e9d0d8..3053c24b7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -779,8 +779,7 @@ trait Messages if (!empty($sRawBody)) { $sMailTo = \trim($aToCollection->ToString(true)); $sMailSubject = \trim($oMessage->GetSubject()); - $sMailSubject = 0 === \strlen($sMailSubject) ? '' : \MailSo\Base\Utils::EncodeUnencodedValue( - \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, $sMailSubject); + $sMailSubject = \strlen($sMailSubject) ? \MailSo\Base\Utils::EncodeHeaderValue($sMailSubject) : ''; $sMailHeaders = $sMailBody = ''; list($sMailHeaders, $sMailBody) = \explode("\r\n\r\n", $sRawBody, 2); From 9558456bcf4e4c4accdb6e3fb6706f57ca651f0f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 11:07:07 +0100 Subject: [PATCH 017/540] Changes for #1397 --- .../libraries/RainLoop/Actions/Messages.php | 88 +++++++++++++------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 3053c24b7..8ce3917d7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -156,7 +156,7 @@ trait Messages $oConfig = $this->Config(); - $sSentFolder = $this->GetActionParam('saveFolder', ''); + $sSaveFolder = $this->GetActionParam('saveFolder', ''); $aDraftInfo = $this->GetActionParam('draftInfo', null); $oMessage = $this->buildMessage($oAccount, false); @@ -174,9 +174,10 @@ trait Messages ); if (false !== $iMessageStreamSize) { - $bDsn = !empty($this->GetActionParam('dsn', 0)); - $bRequireTLS = !empty($this->GetActionParam('requireTLS', 0)); - $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, true, $bDsn, $bRequireTLS); + $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, true, + !empty($this->GetActionParam('dsn', 0)), + !empty($this->GetActionParam('requireTLS', 0)) + ); if (\is_array($aDraftInfo) && 3 === \count($aDraftInfo)) { $sDraftInfoType = $aDraftInfo[0]; @@ -202,43 +203,74 @@ trait Messages } } - if (\strlen($sSentFolder)) { + if (\strlen($sSaveFolder)) { + $rAppendMessageStream = $rMessageStream; + $iAppendMessageStreamSize = $iMessageStreamSize; try { - if (!$oMessage->GetBcc()) { - if (\is_resource($rMessageStream)) { - \rewind($rMessageStream); - } - - $this->Plugins()->RunHook('filter.send-message-stream', - array($oAccount, &$rMessageStream, &$iMessageStreamSize)); - - $this->ImapClient()->MessageAppendStream( - $sSentFolder, $rMessageStream, $iMessageStreamSize, array(MessageFlag::SEEN) - ); - } else { + if ($oMessage->GetBcc()) { $rAppendMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); - $iAppendMessageStreamSize = \MailSo\Base\Utils::WriteStream( $oMessage->ToStream(false), $rAppendMessageStream, 8192, true, true ); - - $this->Plugins()->RunHook('filter.send-message-stream', - array($oAccount, &$rAppendMessageStream, &$iAppendMessageStreamSize)); - - $this->ImapClient()->MessageAppendStream( - $sSentFolder, $rAppendMessageStream, $iAppendMessageStreamSize, array(MessageFlag::SEEN) - ); - - if (\is_resource($rAppendMessageStream)) { - fclose($rAppendMessageStream); + } else { + if (\is_resource($rMessageStream)) { + \rewind($rMessageStream); } } } catch (\Throwable $oException) { + $this->logException($oException, \LOG_ERR); throw new ClientException(Notifications::CantSaveMessage, $oException); } + + try + { + $this->Plugins()->RunHook('filter.send-message-stream', + array($oAccount, &$rAppendMessageStream, &$iAppendMessageStreamSize)); + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + } + + try + { + $this->ImapClient()->MessageAppendStream( + $sSaveFolder, $rAppendMessageStream, $iAppendMessageStreamSize, + array(MessageFlag::SEEN) + ); + } + catch (\Throwable $oException) + { + // Save folder not the same as default Sent folder, so try again + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + if ($oSettingsLocal instanceof \RainLoop\Settings) { + $sSentFolder = (string) $oSettingsLocal->GetConf('SentFolder', ''); + if (\strlen($sSentFolder) && $sSentFolder !== $sSaveFolder) { + $oException = null; + try + { + $this->ImapClient()->MessageAppendStream( + $sSentFolder, $rAppendMessageStream, $iAppendMessageStreamSize, + array(MessageFlag::SEEN) + ); + } + catch (\Throwable $oException) + { + } + } + } + if ($oException) { + $this->logException($oException, \LOG_ERR); + throw new ClientException(Notifications::CantSaveMessage, $oException); + } + } + + if (\is_resource($rAppendMessageStream) && $rAppendMessageStream !== $rMessageStream) { + \fclose($rAppendMessageStream); + } } if (\is_resource($rMessageStream)) { From b7bab3cf21f5f86b4b205008edf5206d6daa9c7f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 11:07:21 +0100 Subject: [PATCH 018/540] bugfix: undefined MimePart --- snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php index f03fd9ba8..ceb2e2692 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php @@ -185,18 +185,18 @@ class Part public function addPgpEncrypted(string $sEncrypted) { - $oPart = new MimePart; + $oPart = new self; $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/encrypted; protocol="application/pgp-encrypted"'); $this->SubParts->append($oPart); - $oSubPart = new MimePart; + $oSubPart = new self; $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); $oPart->SubParts->append($oSubPart); - $oSubPart = new MimePart; + $oSubPart = new self; $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); @@ -206,7 +206,7 @@ class Part public function addPlain(string $sPlain) { - $oPart = new MimePart; + $oPart = new self; $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'text/plain; charset=utf-8'); $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); $oPart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( From e133cf5ffb9f57054d64b35f185fad67039155f4 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 13:25:04 +0100 Subject: [PATCH 019/540] Changes for BIMI-Selector #1394 --- plugins/README.md | 6 ++++++ plugins/avatars/index.php | 14 ++++++++++++-- .../MailSo/Imap/Enumerations/FetchType.php | 14 +++++--------- .../0.0.0/app/libraries/MailSo/Mail/MailClient.php | 12 ++++++------ 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/plugins/README.md b/plugins/README.md index e19e799a1..86d513c48 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -164,6 +164,12 @@ $Plugin->addHook('hook.name', 'functionName'); bool $bSuccess \MailSo\Imap\Settings $oSettings +### imap.message-headers + params: + array &$aHeaders + + Allows you to fetch more MIME headers for messages. + ## Sieve ### sieve.before-connect diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 5e9854a16..3b5d5dc8f 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.14', - RELEASE = '2024-01-17', + VERSION = '1.15', + RELEASE = '2024-01-22', REQUIRED = '2.25.0', CATEGORY = 'Contacts', LICENSE = 'MIT', @@ -32,6 +32,16 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addHook('json.after-message', 'JsonMessage'); $this->addHook('json.after-messagelist', 'JsonMessageList'); } + // https://www.ietf.org/archive/id/draft-brand-indicators-for-message-identification-04.html#bimi-selector + if ($this->Config()->Get('plugin', 'bimi', false)) { + $this->addHook('imap.message-headers', 'ImapMessageHeaders'); + } + } + + public function ImapMessageHeaders(array &$aHeaders) + { + // \MailSo\Mime\Enumerations\Header::BIMI_SELECTOR + $aHeaders[] = 'BIMI-Selector'; } public function JsonMessage(array &$aResponse) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php index ad45b0591..20b30c6ab 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FetchType.php @@ -61,15 +61,11 @@ abstract class FetchType public static function BuildBodyCustomHeaderRequest(array $aHeaders, bool $bPeek = true) : string { - $sResult = ''; - if (\count($aHeaders)) - { - $aHeaders = \array_map('strtoupper', \array_map('trim', $aHeaders)); - - $sResult = $bPeek ? self::BODY_PEEK : self::BODY; - $sResult .= '[HEADER.FIELDS ('.\implode(' ', $aHeaders).')]'; + if (\count($aHeaders)) { + $aHeaders = \array_map(fn($sHeader) => \strtoupper(\trim($sHeader)), $aHeaders); + return ($bPeek ? self::BODY_PEEK : self::BODY) + . '[HEADER.FIELDS ('.\implode(' ', $aHeaders).')]'; } - - return $sResult; + return ''; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index 994ed4feb..78fe31e07 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -46,7 +46,7 @@ class MailClient return FetchType::BODY_HEADER_PEEK; } - return FetchType::BuildBodyCustomHeaderRequest(array( + $aHeaders = array( // MimeHeader::RETURN_PATH, // MimeHeader::RECEIVED, // MimeHeader::MIME_VERSION, @@ -75,8 +75,6 @@ class MailClient MimeHeader::LIST_UNSUBSCRIBE, // https://autocrypt.org/level1.html#the-autocrypt-header MimeHeader::AUTOCRYPT, - // https://www.ietf.org/archive/id/draft-brand-indicators-for-message-identification-04.html#bimi-selector - MimeHeader::BIMI_SELECTOR, // SPAM MimeHeader::X_SPAM_STATUS, MimeHeader::X_SPAM_FLAG, @@ -87,9 +85,11 @@ class MailClient MimeHeader::X_VIRUS, MimeHeader::X_VIRUS_SCANNED, MimeHeader::X_VIRUS_STATUS - ), true); -// -// return FetchType::ENVELOPE; + ); + + \RainLoop\Api::Actions()->Plugins()->RunHook('imap.message-headers', array(&$aHeaders)); + + return FetchType::BuildBodyCustomHeaderRequest($aHeaders, true); } /** From f0bb3a8a45ca2fd7548768deff27bc6788994343 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 13:33:00 +0100 Subject: [PATCH 020/540] Handle message hasVirus header in JavaScript --- dev/Model/Message.js | 19 ++++++++++++-- .../app/libraries/MailSo/Mail/Message.php | 25 ------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/dev/Model/Message.js b/dev/Model/Message.js index 000635e34..fd407b3f2 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -257,13 +257,28 @@ export class MessageModel extends AbstractModel { } // Unsubscribe links - value = headers.valueByName('List-Unsubscribe'); - if (value) { + if (value = headers.valueByName('List-Unsubscribe')) { this.unsubsribeLinks(value.split(',').map( link => link.replace(/^[ <>]+|[ <>]+$/g, '') )); } + if (headers.valueByName('X-Virus')) { + this.hasVirus(true); + } + if (value = headers.valueByName('X-Virus-Status')) { + if (value.includes('infected')) { + this.hasVirus(true); + } else if (value.includes('clean')) { + this.hasVirus(false); + } + } +/* + if (value = headers.valueByName('X-Virus-Scanned')) { + this.virusScanned(value); + } +*/ + return true; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index 6e470f098..4f5954657 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -27,7 +27,6 @@ class Message implements \JsonSerializable $sMessageId = '', $sContentType = '', $sSpamResult = '', - $sVirusScanned = '', $InReplyTo = '', $sPlain = '', $sHtml = '', @@ -57,14 +56,6 @@ class Message implements \JsonSerializable private bool $bIsSpam = false; - private ?bool - /** - * null = not scanned - * true = scanned and infected - * false = scanned and no infection found - */ - $bHasVirus = null; - private array $SPF = [], $DKIM = [], @@ -246,20 +237,6 @@ class Message implements \JsonSerializable || false !== \stripos($oHeaders->ValueByName(MimeHeader::X_SPAM_FLAG), 'YES'); } - if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS)) { - $oMessage->bHasVirus = true; - } - if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS_STATUS)) { - if (false !== \stripos($spam, 'infected')) { - $oMessage->bHasVirus = true; - } else if (false !== \stripos($spam, 'clean')) { - $oMessage->bHasVirus = false; - } - } - if ($virus = $oHeaders->ValueByName(MimeHeader::X_VIRUS_SCANNED)) { - $oMessage->sVirusScanned = $virus; - } - $sDraftInfo = $oHeaders->ValueByName(MimeHeader::X_DRAFT_INFO); if (\strlen($sDraftInfo)) { $sType = ''; @@ -510,8 +487,6 @@ class Message implements \JsonSerializable 'spamScore' => $this->bIsSpam ? 100 : $this->SpamScore, 'spamResult' => $this->sSpamResult, 'isSpam' => $this->bIsSpam, - 'hasVirus' => $this->bHasVirus, -// 'virusScanned' => $this->sVirusScanned, 'dateTimestamp' => $this->iHeaderTimeStampInUTC ?: $this->iInternalTimeStampInUTC, 'internalTimestamp' => $this->iInternalTimeStampInUTC, From 69d92ee184b7f7652a7c1d3b2b072b0713b9da2b Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 14:03:09 +0100 Subject: [PATCH 021/540] workaround with issue to detect dkim --- .../v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php | 1 + 1 file changed, 1 insertion(+) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php index 8d698dafd..bd385f87c 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php @@ -205,6 +205,7 @@ class HeaderCollection extends \MailSo\Base\Collection if (\count($aHeaders)) { $aHeaders = \implode(';', $aHeaders); $aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders); + $aHeaders = \str_replace('-bit key;', '-bit key,', $aHeaders); $aHeaders = \explode(';', $aHeaders); foreach ($aHeaders as $sLine) { $aStatus = array(); From aca34903310a3ae65919c03728cc3828ff3f215d Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 14:30:53 +0100 Subject: [PATCH 022/540] Resolve #1395 and #1394 --- plugins/avatars/avatars.js | 13 +++++-------- plugins/avatars/index.php | 12 ++++++------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index d9f07de74..3c252e848 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -45,11 +45,9 @@ bimiSelector = bimiSelector ? bimiSelector.match(/;.*s=([^\s;]+)/)[1] : ''; return bimiSelector || ''; }, - getAvatarUid = msg => { - let from = msg.from[0], - bimi = ('pass' == from.dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg); - return `${bimi}/${from.email.toLowerCase()}`; - }, + getBimiId = msg => ('pass' == msg.from[0].dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg), + getAvatarUrl = msg => `?Avatar/${getBimiId(msg)}/${msg.avatar}`, + getAvatarUid = msg => `${getBimiId(msg)}/${msg.from[0].email.toLowerCase()}`, getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)), hash = async txt => { if (/^[0-9a-f]{15,}$/i.test(txt)) { @@ -181,7 +179,7 @@ fn(msg.avatar); } else { element.onerror = () => setIdenticon(from, fn); - fn(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${msg.avatar}`); + fn(getAvatarUrl(msg)); } } else { addQueue(msg, fn); @@ -221,8 +219,7 @@ if (url) { fn(url); } else if (msg.avatar) { - fn(msg.avatar.startsWith('data:') ? msg.avatar - : `?Avatar/${'pass' == msg.from[0].dkimStatus ? 1 : 0}/${msg.avatar}`); + fn(msg.avatar.startsWith('data:') ? msg.avatar : getAvatarUrl(msg)); } else { // let from = msg.from[0]; // view.viewUserPic(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${encodeURIComponent(from.email)}`); diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 3b5d5dc8f..c531a5381 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.15', - RELEASE = '2024-01-22', + VERSION = '1.14', + RELEASE = '2024-01-17', REQUIRED = '2.25.0', CATEGORY = 'Contacts', LICENSE = 'MIT', @@ -69,10 +69,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $mFrom = $mFrom->jsonSerialize(); } if (\is_array($mFrom)) { - if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) { - // 'data:image/png;base64,[a-zA-Z0-9+/=]' - return static::getServiceIcon($mFrom['email']); - } if (!$this->Config()->Get('plugin', 'delay', true) && ($this->Config()->Get('plugin', 'gravatar', false) || ($this->Config()->Get('plugin', 'bimi', false) && 'pass' == $mFrom['dkimStatus']) @@ -84,6 +80,10 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } catch (\Throwable $e) { \SnappyMail\Log::error('Crypt', $e->getMessage()); } + if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) { + // 'data:image/png;base64,[a-zA-Z0-9+/=]' + return static::getServiceIcon($mFrom['email']); + } } return null; } From a80cb495d298996cabf5a49c2e028882886b18d2 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 14:34:35 +0100 Subject: [PATCH 023/540] Resolve #1396 --- plugins/avatars/index.php | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index c531a5381..84ec09f46 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -72,7 +72,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin if (!$this->Config()->Get('plugin', 'delay', true) && ($this->Config()->Get('plugin', 'gravatar', false) || ($this->Config()->Get('plugin', 'bimi', false) && 'pass' == $mFrom['dkimStatus']) - || !$this->Config()->Get('plugin', 'service', true) ) ) try { // Base64Url From 5e012bd66e32227d13f5952b5ed367a22ae07bd5 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 14:56:05 +0100 Subject: [PATCH 024/540] Bugfix: redesign of addPgpEncrypted() failed --- .../v/0.0.0/app/libraries/RainLoop/Actions/Messages.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 8ce3917d7..49139c2b3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -1056,7 +1056,7 @@ trait Messages unset($sSigned); } else if ($sEncrypted = $this->GetActionParam('encrypted', '')) { - $oPart->addPgpEncrypted(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sEncrypted))); + $oMessage->addPgpEncrypted(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sEncrypted))); unset($sEncrypted); } else if ($sHtml = $this->GetActionParam('html', '')) { @@ -1222,7 +1222,7 @@ trait Messages $GPG->addEncryptKey($sFingerprint); } - $oPart->addPgpEncrypted($GPG->encryptStream($fp)); + $oMessage->addPgpEncrypted($GPG->encryptStream($fp)); } $this->Plugins()->RunHook('filter.build-message', array($oMessage)); From 61c533a1a66d096648116ad7604125008d8ae0ab Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 22 Jan 2024 15:11:44 +0100 Subject: [PATCH 025/540] v2.33.0 --- CHANGELOG.md | 36 +++++++++++++++++++ README.md | 26 +++++++------- integrations/cloudron/DESCRIPTION.md | 2 +- integrations/cloudron/Dockerfile | 2 +- .../nextcloud/snappymail/appinfo/info.xml | 2 +- integrations/virtualmin/snappymail.pl | 2 +- package.json | 2 +- plugins/demo-account/index.php | 4 +-- 8 files changed, 56 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdb6b5e8a..062680c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +## 2.33.0 – 2024-01-22 + +### Added +- Feature to use the SQLite AddressBook per login account instead of global (on by default). +- Return all fetched messages headers in JSON. + +### Changed +- Docker hub use Alpine linux 3.18.5 and PHP 8.2 +- Some InvalidArgumentException to the better suited ValueError +- Removed some unused KnockoutJS code +- KnockoutJS drop unused rateLimit method +- Cleanup some data-bind="" +- Drop the disabled KnockoutJS twoWayBindings +- Drop support for KnockoutJS _ko_property_writers and for two-way binding they must be observables +- Login form use method="POST" to prevent uri exposure when javascript fails +- Merge code to generate MIME PGP parts and MIME Plain parts +- SMTP sendRequestWithCheck for future support of RFC's +- Cleanup mime header handling + +### Fixed +- Sorting not supported since 2.32.0 + [#1373](https://github.com/the-djmaze/snappymail/issues/1373) +- FILE_ON_SAVING_ERROR is not defined + [#1379](https://github.com/the-djmaze/snappymail/issues/1379) +- Saving EML files with same subject result in only saving latest email + [#1381](https://github.com/the-djmaze/snappymail/issues/1381) +- Some Sieve parser issues +- Handling of RainLoop Sieve script +- Sieve rfc5429 RejectCommand and ErejectCommand +- KnockoutJS title:value was removed, use attr:{title:value} +- dataBaseUpgrade() always runs on sqlite and pgsql +- Message was sent but not saved to sent items folder + [#1397](https://github.com/the-djmaze/snappymail/issues/1397) +- DKIM `pass` detection sometimes failed + + ## 2.32.0 – 2023-12-26 ### Added diff --git a/README.md b/README.md index 8291c7ac9..17668c1f4 100644 --- a/README.md +++ b/README.md @@ -140,26 +140,26 @@ RainLoop 1.17 vs SnappyMail |js/* |RainLoop |Snappy | |--------------- |--------: |--------: | -|admin.js |2.170.153 | 80.370 | -|app.js |4.207.787 | 408.429 | +|admin.js |2.170.153 | 80.563 | +|app.js |4.207.787 | 410.974 | |boot.js | 868.735 | 4.142 | -|libs.js | 658.812 | 193.230 | -|sieve.js | 0 | 85.085 | +|libs.js | 658.812 | 192.606 | +|sieve.js | 0 | 84.707 | |polyfills.js | 334.608 | 0 | |serviceworker.js | 0 | 285 | -|TOTAL |8.240.095 | 771.541 | +|TOTAL |8.240.095 | 773.277 | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | -|admin.min.js | 256.831 | 39.285 | 73.606 | 13.183 | 60.877 | 11.806 | -|app.min.js | 515.367 | 186.270 |139.456 | 63.111 |110.485 | 54.218 | +|admin.min.js | 256.831 | 39.461 | 73.606 | 13.218 | 60.877 | 11.841 | +|app.min.js | 515.367 | 187.193 |139.456 | 63.375 |110.485 | 54.428 | |boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 | -|libs.min.js | 584.772 | 93.401 |180.901 | 34.765 |155.182 | 31.194 | -|sieve.min.js | 0 | 41.316 | 0 | 10.364 | 0 | 9.352 | +|libs.min.js | 584.772 | 92.780 |180.901 | 34.489 |155.182 | 30.953 | +|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 | |polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | -|TOTAL user |1.217.635 | 281.755 |358.761 | 99.078 |299.485 | 86.415 | -|TOTAL user+sieve |1.217.635 | 323.071 |358.761 |109.442 |299.485 | 95.767 | -|TOTAL admin | 959.099 | 134.770 |292.911 | 49.150 |249.877 | 44.003 | +|TOTAL user |1.217.635 | 282.057 |358.761 | 99.066 |299.485 | 86.384 | +|TOTAL user+sieve |1.217.635 | 323.221 |358.761 |109.431 |299.485 | 95.743 | +|TOTAL admin | 959.099 | 134.325 |292.911 | 48.909 |249.877 | 43.797 | For a user it is around 69% smaller and faster than traditional RainLoop. @@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development |OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |-------: |--------: |--------: | -|openpgp.min.js | 330.742 | 541.176 |102.388 | 168.266 | 84.241 | 138.278 | +|openpgp.min.js | 330.742 | 540.792 |102.388 | 167.971 | 84.241 | 138.010 | |openpgp.worker | 1.499 | | 824 | | 695 | | diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md index b45d6664c..7d76a1e2d 100644 --- a/integrations/cloudron/DESCRIPTION.md +++ b/integrations/cloudron/DESCRIPTION.md @@ -1,4 +1,4 @@ -This app packages SnappyMail 2.32.0. +This app packages SnappyMail 2.33.0. SnappyMail is a simple, modern, lightweight & fast web-based email client. diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile index c6b023dab..4304ab0eb 100644 --- a/integrations/cloudron/Dockerfile +++ b/integrations/cloudron/Dockerfile @@ -4,7 +4,7 @@ RUN mkdir -p /app/code WORKDIR /app/code # If you change the extraction below, be sure to test on scaleway -VERSION=2.32.0 +VERSION=2.33.0 RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \ unzip /tmp/snappymail.zip -d /app/code && \ rm /tmp/snappymail.zip && \ diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml index 029bee605..4626297e0 100644 --- a/integrations/nextcloud/snappymail/appinfo/info.xml +++ b/integrations/nextcloud/snappymail/appinfo/info.xml @@ -3,7 +3,7 @@ snappymail SnappyMail SnappyMail Webmail - 2.32.0 + 2.33.0 agpl SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli Date: Tue, 23 Jan 2024 10:20:54 +0100 Subject: [PATCH 026/540] Bugfix: ask/send readReceipt was broken --- dev/View/User/MailBox/MessageView.js | 8 ++-- .../app/libraries/MailSo/Mail/Message.php | 38 +++++++++---------- .../libraries/RainLoop/Actions/Messages.php | 7 ++-- .../libraries/RainLoop/Actions/Response.php | 2 +- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index f655262bf..a5e167f60 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -155,10 +155,10 @@ export class MailMessageView extends AbstractViewRight { : '' ).join(' '), - askReadReceipt: () => - (MessagelistUserStore.isDraftFolder() || MessagelistUserStore.isSentFolder()) - && currentMessage()?.readReceipt() - && currentMessage()?.flags().includes('$mdnsent'), + askReadReceipt: () => currentMessage()?.readReceipt() + && !(MessagelistUserStore.isDraftFolder() || MessagelistUserStore.isSentFolder()) + && !currentMessage()?.flags().includes('$mdnsent') + && !currentMessage()?.flags().includes('\\answered'), listAttachments: () => currentMessage()?.attachments() .filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()), diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index 4f5954657..b80ac0e5e 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -197,10 +197,23 @@ class Message implements \JsonSerializable // $oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(MimeHeader::RETURN_RECEIPT_TO)); // Read Receipt - $oMessage->ReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::DISPOSITION_NOTIFICATION_TO)); - if (empty($oMessage->ReadReceipt)) { - $oMessage->ReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::X_CONFIRM_READING_TO)); + $sReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::DISPOSITION_NOTIFICATION_TO)); + if (empty($sReadReceipt)) { + $sReadReceipt = \trim($oHeaders->ValueByName(MimeHeader::X_CONFIRM_READING_TO)); } + if ($sReadReceipt) { + try + { + if (!\MailSo\Mime\Email::Parse($sReadReceipt)) { + $sReadReceipt = ''; + } + } + catch (\Throwable $oException) + { + $sReadReceipt = ''; + } + } + $oMessage->ReadReceipt = $sReadReceipt; if ($spam = $oHeaders->ValueByName(MimeHeader::X_SPAMD_RESULT)) { if (\preg_match('/\\[([\\d\\.-]+)\\s*\\/\\s*([\\d\\.]+)\\];/', $spam, $match)) { @@ -461,21 +474,6 @@ class Message implements \JsonSerializable } } - $aFlags = $this->getFlags(); - $sReadReceipt = $this->ReadReceipt; - if (\strlen($sReadReceipt) && !\in_array('$forwarded', $aFlags)) { - try - { - if (!\MailSo\Mime\Email::Parse($sReadReceipt)) { - $sReadReceipt = ''; - } - } - catch (\Throwable $oException) - { - $sReadReceipt = ''; - } - } - $result = array( '@Object' => 'Object/Message', 'folder' => $this->sFolder, @@ -499,7 +497,7 @@ class Message implements \JsonSerializable 'sender' => $this->oSender, 'deliveredTo' => $this->oDeliveredTo, - 'readReceipt' => $sReadReceipt, + 'readReceipt' => $this->ReadReceipt, 'autocrypt' => $aAutocrypt ?: null, 'attachments' => $this->Attachments, @@ -508,7 +506,7 @@ class Message implements \JsonSerializable 'dkim' => $this->DKIM, 'dmarc' => $this->DMARC, - 'flags' => $aFlags, + 'flags' => $this->getFlags(), 'inReplyTo' => $this->InReplyTo, diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 49139c2b3..af23dbef1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -368,14 +368,15 @@ trait Messages $sFolderFullName = $this->GetActionParam('messageFolder', ''); $iUid = (int) $this->GetActionParam('messageUid', 0); - $this->Cacher($oAccount)->Set(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $sFolderFullName, $iUid), '1'); - if (\strlen($sFolderFullName) && 0 < $iUid) { try { $this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), MessageFlag::MDNSENT, true, true); } - catch (\Throwable $oException) {} + catch (\Throwable $oException) + { + $this->Cacher($oAccount)->Set(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $sFolderFullName, $iUid), '1'); + } } } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index 821bac496..446d3fcc4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -96,7 +96,7 @@ trait Response if (!$this->Config()->Get('labs', 'date_from_headers', true) && $aResult['internalTimestamp']) { $aResult['dateTimestamp'] = $aResult['internalTimestamp']; } - if (!$sParent && \strlen($aResult['readReceipt']) && !\in_array('$forwarded', $aResult['flags'])) { + if (!$sParent && \strlen($aResult['readReceipt']) && !\in_array('$mdnsent', $aResult['flags']) && !\in_array('\\answered', $aResult['flags'])) { $oAccount = $this->getAccountFromToken(); if ('1' === $this->Cacher($oAccount)->Get(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $aResult['folder'], $aResult['uid']), '0')) { $aResult['readReceipt'] = ''; From b726400f42dc7ec9d8888edbff8fd3f1b41cec83 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 23 Jan 2024 10:23:06 +0100 Subject: [PATCH 027/540] Resolve #1402 --- plugins/avatars/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 84ec09f46..9dfcec75d 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.14', - RELEASE = '2024-01-17', + VERSION = '1.15', + RELEASE = '2024-01-22', REQUIRED = '2.25.0', CATEGORY = 'Contacts', LICENSE = 'MIT', From 29f6b2b743d16eef03addbb7653f5f8af83460fb Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 23 Jan 2024 10:38:54 +0100 Subject: [PATCH 028/540] Move Autocrypt parsing to JavaScript --- dev/Model/Message.js | 11 +++++++++-- .../v/0.0.0/app/libraries/MailSo/Mail/Message.php | 15 +-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/dev/Model/Message.js b/dev/Model/Message.js index fd407b3f2..0c886cbe5 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -74,7 +74,7 @@ export class MessageModel extends AbstractModel { this.messageId = ''; this.inReplyTo = ''; this.references = ''; - this.autocrypt = {}; + this.autocrypt = {/*addr:'', 'prefer-encrypt':'nopreference', keydata:'BASE64'*/}; addObservablesTo(this, { subject: '', @@ -277,8 +277,15 @@ export class MessageModel extends AbstractModel { if (value = headers.valueByName('X-Virus-Scanned')) { this.virusScanned(value); } -*/ + // https://autocrypt.org/level1.html#the-autocrypt-header + if (value = headers.valueByName('Autocrypt')) { + value.split(';').forEach(entry => { + entry = entry.trim().split('=', 2); + json.autocrypt[entry[0]] = entry[1]; + }); + } +*/ return true; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index b80ac0e5e..1e0907e17 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -42,9 +42,7 @@ class Message implements \JsonSerializable /** * https://www.rfc-editor.org/rfc/rfc8970 */ - $sPreview = null, - // https://autocrypt.org/level1.html#the-autocrypt-header - $sAutocrypt = ''; + $sPreview = null; private int $Uid = 0, @@ -291,8 +289,6 @@ class Message implements \JsonSerializable } } } - - $oMessage->sAutocrypt = $oHeaders->ValueByName(MimeHeader::AUTOCRYPT); } else if ($oFetchResponse->GetEnvelope()) { @@ -466,14 +462,6 @@ class Message implements \JsonSerializable $this->aFlagsLowerCase ), true); */ - $aAutocrypt = []; - if ($this->sAutocrypt) { - foreach (\explode(';', $this->sAutocrypt) as $entry) { - $entry = \explode('=', \trim($entry), 2); - $aAutocrypt[$entry[0]] = $entry[1]; - } - } - $result = array( '@Object' => 'Object/Message', 'folder' => $this->sFolder, @@ -498,7 +486,6 @@ class Message implements \JsonSerializable 'deliveredTo' => $this->oDeliveredTo, 'readReceipt' => $this->ReadReceipt, - 'autocrypt' => $aAutocrypt ?: null, 'attachments' => $this->Attachments, From 443a6df070b957317986e458db6b3d4fcf00712d Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 23 Jan 2024 14:34:41 +0100 Subject: [PATCH 029/540] Small change to show why saving message to Sent mailbox failed #1397 --- dev/View/Popup/Compose.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 465955c61..04cd423fc 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -487,7 +487,11 @@ export class ComposePopupView extends AbstractViewPopup { if (iError) { if (Notifications.CantSaveMessage === iError) { this.sendSuccessButSaveError(true); - this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim()); + let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND'); + if (data?.ErrorMessageAdditional) { + msg = msg + "\n" + data?.ErrorMessageAdditional; + } + this.savedErrorDesc(msg); } else { this.sendError(true); this.sendErrorDesc(getNotification(iError, data?.ErrorMessage) From 4b8ad326fa3bf0a4f73e924b135b64a3fa99a25e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 23 Jan 2024 14:35:41 +0100 Subject: [PATCH 030/540] Cleanup MIME header email address parsing --- .../libraries/MailSo/Mime/EmailCollection.php | 9 --- .../app/libraries/MailSo/Mime/Header.php | 5 -- .../app/libraries/MailSo/Mime/Message.php | 61 ++++++++----------- .../libraries/RainLoop/Actions/Messages.php | 6 +- 4 files changed, 27 insertions(+), 54 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php index 11d80c562..0d56809af 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php @@ -33,15 +33,6 @@ class EmailCollection extends \MailSo\Base\Collection parent::append($oEmail, $bToTop); } - public function MergeWithOtherCollection(EmailCollection $oEmails) : self - { - foreach ($oEmails as $oEmail) { - $this->append($oEmail); - } - - return $this; - } - public function Unique() : self { $aReturn = array(); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php index eff1a3275..aeb60de49 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php @@ -196,11 +196,6 @@ class Header implements \JsonSerializable return $this->Value(); } - private function IsReparsed() : bool - { - return $this->IsEmail() || $this->IsSubject() || $this->IsParameterized(); - } - #[\ReturnTypeWillChange] public function jsonSerialize() { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php index e13ddb553..04d7fea14 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Message.php @@ -51,6 +51,13 @@ class Message extends Part $this->oAttachmentCollection = new AttachmentCollection; } + private function getHeaderValue(string $name) + { + return isset($this->aHeadersValue[$name]) + ? $this->aHeadersValue[$name] + : null; + } + public function DoesNotAddDefaultXMailer() : void { $this->bAddDefaultXMailer = false; @@ -58,8 +65,7 @@ class Message extends Part public function MessageId() : string { - return empty($this->aHeadersValue[Enumerations\Header::MESSAGE_ID]) ? '' - : $this->aHeadersValue[Enumerations\Header::MESSAGE_ID]; + return $this->getHeaderValue(Enumerations\Header::MESSAGE_ID) ?: ''; } public function SetMessageId(string $sMessageId) : void @@ -79,52 +85,31 @@ class Message extends Part public function GetSubject() : string { - return isset($this->aHeadersValue[Enumerations\Header::SUBJECT]) ? - $this->aHeadersValue[Enumerations\Header::SUBJECT] : ''; + return $this->getHeaderValue(Enumerations\Header::SUBJECT) ?: ''; } public function GetFrom() : ?Email { - if (isset($this->aHeadersValue[Enumerations\Header::FROM_]) && - $this->aHeadersValue[Enumerations\Header::FROM_] instanceof Email) - { - return $this->aHeadersValue[Enumerations\Header::FROM_]; - } - - return null; + $value = $this->getHeaderValue(Enumerations\Header::FROM_); + return ($value instanceof Email) ? $value : null; } - public function GetTo() : EmailCollection + public function GetTo() : ?EmailCollection { - if (isset($this->aHeadersValue[Enumerations\Header::TO_]) && - $this->aHeadersValue[Enumerations\Header::TO_] instanceof EmailCollection) - { - return $this->aHeadersValue[Enumerations\Header::TO_]->Unique(); - } - - return new EmailCollection; + $value = $this->getHeaderValue(Enumerations\Header::TO_); + return ($value instanceof EmailCollection) ? $value->Unique() : null; } public function GetCc() : ?EmailCollection { - if (isset($this->aHeadersValue[Enumerations\Header::CC]) && - $this->aHeadersValue[Enumerations\Header::CC] instanceof EmailCollection) - { - return $this->aHeadersValue[Enumerations\Header::CC]->Unique(); - } - - return null; + $value = $this->getHeaderValue(Enumerations\Header::CC); + return ($value instanceof EmailCollection) ? $value->Unique() : null; } public function GetBcc() : ?EmailCollection { - if (isset($this->aHeadersValue[Enumerations\Header::BCC]) && - $this->aHeadersValue[Enumerations\Header::BCC] instanceof EmailCollection) - { - return $this->aHeadersValue[Enumerations\Header::BCC]->Unique(); - } - - return null; + $value = $this->getHeaderValue(Enumerations\Header::BCC); + return ($value instanceof EmailCollection) ? $value->Unique() : null; } public function GetRcpt() : EmailCollection @@ -133,8 +118,9 @@ class Message extends Part $headers = array(Enumerations\Header::TO_, Enumerations\Header::CC, Enumerations\Header::BCC); foreach ($headers as $header) { - if (isset($this->aHeadersValue[$header]) && $this->aHeadersValue[$header] instanceof EmailCollection) { - foreach ($this->aHeadersValue[$header] as $oEmail) { + $value = $this->getHeaderValue($header); + if ($value instanceof EmailCollection) { + foreach ($value as $oEmail) { $oResult->append($oEmail); } } @@ -144,8 +130,9 @@ class Message extends Part $aReturn = array(); $headers = array(Enumerations\Header::TO_, Enumerations\Header::CC, Enumerations\Header::BCC); foreach ($headers as $header) { - if (isset($this->aHeadersValue[$header]) && $this->aHeadersValue[$header] instanceof EmailCollection) { - foreach ($this->aHeadersValue[$header] as $oEmail) { + $value = $this->getHeaderValue($header); + if ($value instanceof EmailCollection) { + foreach ($value as $oEmail) { $oResult->append($oEmail); $sEmail = $oEmail->GetEmail(); if (!isset($aReturn[$sEmail])) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index af23dbef1..6552ec0c1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -806,11 +806,11 @@ trait Messages if ($oSmtpClient->Settings->usePhpMail) { if (\MailSo\Base\Utils::FunctionCallable('mail')) { - $aToCollection = $oMessage->GetTo(); - if ($aToCollection && $oFrom) { + $oToCollection = $oMessage->GetTo(); + if ($oToCollection && $oFrom) { $sRawBody = \stream_get_contents($rMessageStream); if (!empty($sRawBody)) { - $sMailTo = \trim($aToCollection->ToString(true)); + $sMailTo = \trim($oToCollection->ToString(true)); $sMailSubject = \trim($oMessage->GetSubject()); $sMailSubject = \strlen($sMailSubject) ? \MailSo\Base\Utils::EncodeHeaderValue($sMailSubject) : ''; From 91595ef6d5a527c6e521be51bade558e395fe6c0 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 23 Jan 2024 16:35:22 +0100 Subject: [PATCH 031/540] Resolve ##1403 --- .../0.0.0/app/libraries/MailSo/Base/Utils.php | 5 ++- .../app/libraries/MailSo/Mail/MailClient.php | 2 +- .../app/libraries/MailSo/Mail/Message.php | 18 +++++------ .../0.0.0/app/libraries/MailSo/Mime/Email.php | 1 + .../app/libraries/MailSo/Mime/Header.php | 23 +++++++------ .../MailSo/Mime/HeaderCollection.php | 32 ++++++------------- 6 files changed, 36 insertions(+), 45 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php index 3d8793147..1d794ae95 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php @@ -219,7 +219,7 @@ abstract class Utils return \preg_replace('/ ([\r]?[\n])/m', ' ', $sInputValue); } - public static function DecodeHeaderValue(string $sEncodedValue, string $sIncomingCharset = '', string $sForcedIncomingCharset = '') : string + public static function DecodeHeaderValue(string $sEncodedValue, string $sIncomingCharset = '') : string { $sValue = $sEncodedValue; if (\strlen($sIncomingCharset)) { @@ -276,8 +276,7 @@ abstract class Utils } if (\strlen($aTempArr[0])) { - $sCharset = \strlen($sForcedIncomingCharset) ? $sForcedIncomingCharset : $aTempArr[0]; - $sCharset = static::NormalizeCharset($sCharset, true); + $sCharset = static::NormalizeCharset($aTempArr[0], true); if ('' === $sMainCharset) { $sMainCharset = $sCharset; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index 78fe31e07..b70e49906 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -332,7 +332,7 @@ class MailClient 'folder' => $sFolderName, 'uid' => $iUid, 'subject' => $oHeaders->ValueByName(MimeHeader::SUBJECT, !$sContentTypeCharset), - 'from' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, !$sContentTypeCharset) + 'from' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_) ); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index 1e0907e17..cae8fc10d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -151,7 +151,7 @@ class Message implements \JsonSerializable $sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : ''; $sHeaders = $oFetchResponse->GetHeaderFieldsValue(); - $oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null; + $oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, $sCharset) : null; if ($oHeaders) { $oMessage->Headers = $oHeaders; @@ -159,11 +159,9 @@ class Message implements \JsonSerializable MimeHeader::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::CHARSET ); - if (\strlen($sContentTypeCharset)) { $sCharset = Utils::NormalizeCharset($sContentTypeCharset); } - if (\strlen($sCharset)) { $oHeaders->SetParentCharset($sCharset); } @@ -174,14 +172,14 @@ class Message implements \JsonSerializable $oMessage->sMessageId = $oHeaders->ValueByName(MimeHeader::MESSAGE_ID); $oMessage->sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE); - $oMessage->oFrom = $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, $bCharsetAutoDetect); - $oMessage->oTo = $oHeaders->GetAsEmailCollection(MimeHeader::TO_, $bCharsetAutoDetect); - $oMessage->oCc = $oHeaders->GetAsEmailCollection(MimeHeader::CC, $bCharsetAutoDetect); - $oMessage->oBcc = $oHeaders->GetAsEmailCollection(MimeHeader::BCC, $bCharsetAutoDetect); + $oMessage->oFrom = $oHeaders->GetAsEmailCollection(MimeHeader::FROM_); + $oMessage->oTo = $oHeaders->GetAsEmailCollection(MimeHeader::TO_); + $oMessage->oCc = $oHeaders->GetAsEmailCollection(MimeHeader::CC); + $oMessage->oBcc = $oHeaders->GetAsEmailCollection(MimeHeader::BCC); - $oMessage->oSender = $oHeaders->GetAsEmailCollection(MimeHeader::SENDER, $bCharsetAutoDetect); - $oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(MimeHeader::REPLY_TO, $bCharsetAutoDetect); - $oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(MimeHeader::DELIVERED_TO, $bCharsetAutoDetect); + $oMessage->oSender = $oHeaders->GetAsEmailCollection(MimeHeader::SENDER); + $oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(MimeHeader::REPLY_TO); + $oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(MimeHeader::DELIVERED_TO); $oMessage->InReplyTo = $oHeaders->ValueByName(MimeHeader::IN_REPLY_TO); $oMessage->References = Utils::StripSpaces( diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php index 88559eff7..66371f9c4 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php @@ -43,6 +43,7 @@ class Email implements \JsonSerializable */ public static function Parse(string $sEmailAddress) : self { + $sEmailAddress = \MailSo\Base\Utils::DecodeHeaderValue($sEmailAddress); $sEmailAddress = \MailSo\Base\Utils::Trim($sEmailAddress); if (!\strlen(\trim($sEmailAddress))) { throw new \ValueError; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php index aeb60de49..200d387cf 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php @@ -23,7 +23,7 @@ class Header implements \JsonSerializable private string $sFullValue; - private string $sEncodedValueForReparse; + private string $sEncodedValue; private ?ParameterCollection $oParameters = null; @@ -39,10 +39,10 @@ class Header implements \JsonSerializable { $this->sName = \trim($sName); $this->sFullValue = \trim($sValue); - $this->sEncodedValueForReparse = ''; + $this->sEncodedValue = ''; if (\strlen($sEncodedValueForReparse) && ($this->IsEmail() || $this->IsSubject() || $this->IsParameterized())) { - $this->sEncodedValueForReparse = \trim($sEncodedValueForReparse); + $this->sEncodedValue = \trim($sEncodedValueForReparse); } if (\strlen($this->sFullValue) && $this->IsParameterized()) { @@ -101,13 +101,18 @@ class Header implements \JsonSerializable return $this->sFullValue; } + public function EncodedValue() : string + { + return $this->sEncodedValue ?: $this->sFullValue; + } + public function SetParentCharset(string $sParentCharset) : Header { - if ($this->sParentCharset !== $sParentCharset && \strlen($this->sEncodedValueForReparse)) { + if ($this->sParentCharset !== $sParentCharset && \strlen($this->sEncodedValue)) { $this->initInputData( $this->sName, - \trim(\MailSo\Base\Utils::DecodeHeaderValue($this->sEncodedValueForReparse, $sParentCharset)), - $this->sEncodedValueForReparse + \trim(\MailSo\Base\Utils::DecodeHeaderValue($this->sEncodedValue, $sParentCharset)), + $this->sEncodedValue ); } @@ -187,9 +192,9 @@ class Header implements \JsonSerializable public function ValueWithCharsetAutoDetect() : string { if (!\MailSo\Base\Utils::IsAscii($this->Value()) - && \strlen($this->sEncodedValueForReparse) - && !\MailSo\Base\Utils::IsAscii($this->sEncodedValueForReparse) - && ($mEncoding = \mb_detect_encoding($this->sEncodedValueForReparse, 'auto', true)) + && \strlen($this->sEncodedValue) + && !\MailSo\Base\Utils::IsAscii($this->sEncodedValue) + && ($mEncoding = \mb_detect_encoding($this->sEncodedValue, 'auto', true)) ) { $this->SetParentCharset($mEncoding); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php index bd385f87c..5b3674dbe 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php @@ -17,16 +17,13 @@ namespace MailSo\Mime; */ class HeaderCollection extends \MailSo\Base\Collection { - - protected string $sRawHeaders = ''; - protected string $sParentCharset = ''; - function __construct(string $sRawHeaders = '', bool $bStoreRawHeaders = true, string $sParentCharset = '') + function __construct(string $sRawHeaders = '', string $sParentCharset = '') { parent::__construct(); if (\strlen($sRawHeaders)) { - $this->Parse($sRawHeaders, $bStoreRawHeaders, $sParentCharset); + $this->Parse($sRawHeaders, $sParentCharset); } } @@ -53,13 +50,13 @@ class HeaderCollection extends \MailSo\Base\Collection return $oHeader ? ($bCharsetAutoDetect ? $oHeader->ValueWithCharsetAutoDetect() : $oHeader->Value()) : ''; } - public function ValuesByName(string $sHeaderName, bool $bCharsetAutoDetect = false) : array + public function ValuesByName(string $sHeaderName) : array { $aResult = array(); $sHeaderNameLower = \strtolower($sHeaderName); foreach ($this as $oHeader) { if ($sHeaderNameLower === \strtolower($oHeader->Name())) { - $aResult[] = $bCharsetAutoDetect ? $oHeader->ValueWithCharsetAutoDetect() : $oHeader->Value(); + $aResult[] = $oHeader->Value(); } } return $aResult; @@ -74,11 +71,12 @@ class HeaderCollection extends \MailSo\Base\Collection return $this; } - public function GetAsEmailCollection(string $sHeaderName, bool $bCharsetAutoDetect = false) : ?EmailCollection + public function GetAsEmailCollection(string $sHeaderName) : ?EmailCollection { - return new EmailCollection( - $this->ValueByName($sHeaderName, $bCharsetAutoDetect) - ); + if ($oHeader = $this->GetByName($sHeaderName)) { + return new EmailCollection($oHeader->EncodedValue()); + } + return new EmailCollection(); } public function ParameterValue(string $sHeaderName, string $sParamName) : string @@ -110,20 +108,10 @@ class HeaderCollection extends \MailSo\Base\Collection return $this; } - public function Clear() : void - { - parent::Clear(); - $this->sRawHeaders = ''; - } - - public function Parse(string $sRawHeaders, bool $bStoreRawHeaders = false, string $sParentCharset = '') : self + public function Parse(string $sRawHeaders, string $sParentCharset = '') : self { $this->Clear(); - if ($bStoreRawHeaders) { - $this->sRawHeaders = $sRawHeaders; - } - if (\strlen($this->sParentCharset)) { $this->sParentCharset = $sParentCharset; } From 3a37aad25f6a7b1f081c3e6767bb82c08fc9fed6 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 23 Jan 2024 16:35:43 +0100 Subject: [PATCH 032/540] Remove unused NameWithDelimitrom() --- .../v/0.0.0/app/libraries/MailSo/Mime/Header.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php index 200d387cf..20c0e3941 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Header.php @@ -86,11 +86,6 @@ class Header implements \JsonSerializable return $this->sName; } - public function NameWithDelimitrom() : string - { - return $this->sName . ': '; - } - public function Value() : string { return $this->sValue; @@ -160,7 +155,8 @@ class Header implements \JsonSerializable } // https://www.rfc-editor.org/rfc/rfc2822#section-2.1.1, avoid folding immediately after the header name - return $this->NameWithDelimitrom() . \wordwrap($sResult, 78 - \strlen($this->NameWithDelimitrom()) - 1, "\r\n "); + $sName = $this->sName . ': '; + return $sName . \wordwrap($sResult, 78 - \strlen($sName) - 1, "\r\n "); } private function IsSubject() : bool @@ -207,7 +203,7 @@ class Header implements \JsonSerializable $aResult = array( '@Object' => 'Object/MimeHeader', 'name' => $this->sName, - 'value' => $this->sValue + 'value' => $this->sValue // $this->EncodedValue() ); if ($this->oParameters->count()) { $aResult['parameters'] = $this->oParameters; From 7eba9cf48e326726ca02392c91aa7f3ce0952c64 Mon Sep 17 00:00:00 2001 From: lmperfis Date: Thu, 25 Jan 2024 16:04:40 +0000 Subject: [PATCH 033/540] Update pt/pt-PT translation --- snappymail/v/0.0.0/app/localization/pt-PT/admin.json | 2 +- snappymail/v/0.0.0/app/localization/pt-PT/user.json | 6 +++--- snappymail/v/0.0.0/app/localization/pt/admin.json | 2 +- snappymail/v/0.0.0/app/localization/pt/user.json | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/snappymail/v/0.0.0/app/localization/pt-PT/admin.json b/snappymail/v/0.0.0/app/localization/pt-PT/admin.json index 68b9a33aa..d9bc0bdd2 100644 --- a/snappymail/v/0.0.0/app/localization/pt-PT/admin.json +++ b/snappymail/v/0.0.0/app/localization/pt-PT/admin.json @@ -68,7 +68,7 @@ "LABEL_STORAGE_DSN": "Nome da Origem de Dados (DSN)", "LABEL_STORAGE_USER": "Utilizador", "LABEL_STORAGE_PASSWORD": "Palavra-passe", - "SQLITE_GLOBAL": "Use a global DB instead of per account", + "SQLITE_GLOBAL": "Usar uma BD global em vez de individual", "ALERT_NOTICE": "Aviso!", "SUGGESTIONS_LIMIT": "Limite de sugestões", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Não use este tipo de base de dados com uma grande quantidade de utilizadores ativos.", diff --git a/snappymail/v/0.0.0/app/localization/pt-PT/user.json b/snappymail/v/0.0.0/app/localization/pt-PT/user.json index b2d30a0e9..c1867413e 100644 --- a/snappymail/v/0.0.0/app/localization/pt-PT/user.json +++ b/snappymail/v/0.0.0/app/localization/pt-PT/user.json @@ -210,7 +210,7 @@ "ADD_MENU_TAGS": "Etiquetas", "BUTTON_SHARE_ALL": "Todos", "BUTTON_SYNC": "Sincronização (CardDAV)", - "SEND_TO_ALL_CONTACT_EMAILS": "Send to all contact emails" + "SEND_TO_ALL_CONTACT_EMAILS": "Enviar para todos os contactos" }, "COMPOSE": { "LINK_SHOW_INPUTS": "Mostrar todos os campos", @@ -255,7 +255,7 @@ "TITLE_UPDATE_IDENTITY": "Atualizar identidade?", "BUTTON_ADD_IDENTITY": "Adicionar", "BUTTON_UPDATE_IDENTITY": "Atualizar", - "LABEL_SIGNATURE_ADD": "Add\/Edit signature", + "LABEL_SIGNATURE_ADD": "Adicionar\/Editar assinatura", "LABEL_SIGNATURE_INSERT_BEFORE": "Nas respostas, inserir esta assinatura acima da mensagem citada" }, "POPUPS_CREATE_FOLDER": { @@ -455,7 +455,7 @@ "IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar", "IMAGES_OPTION_ALWAYS": "Mostrar sempre", "IMAGES_WHITELIST": "Lista Branca de imagens", - "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab" + "MESSAGE_POPUP_WINDOW": "Abrir em nova janela em vez de um separador" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Adicionar destinatários automaticamente à sua lista de endereços", diff --git a/snappymail/v/0.0.0/app/localization/pt/admin.json b/snappymail/v/0.0.0/app/localization/pt/admin.json index 68b9a33aa..d9bc0bdd2 100644 --- a/snappymail/v/0.0.0/app/localization/pt/admin.json +++ b/snappymail/v/0.0.0/app/localization/pt/admin.json @@ -68,7 +68,7 @@ "LABEL_STORAGE_DSN": "Nome da Origem de Dados (DSN)", "LABEL_STORAGE_USER": "Utilizador", "LABEL_STORAGE_PASSWORD": "Palavra-passe", - "SQLITE_GLOBAL": "Use a global DB instead of per account", + "SQLITE_GLOBAL": "Usar uma BD global em vez de individual", "ALERT_NOTICE": "Aviso!", "SUGGESTIONS_LIMIT": "Limite de sugestões", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Não use este tipo de base de dados com uma grande quantidade de utilizadores ativos.", 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 b2d30a0e9..c1867413e 100644 --- a/snappymail/v/0.0.0/app/localization/pt/user.json +++ b/snappymail/v/0.0.0/app/localization/pt/user.json @@ -210,7 +210,7 @@ "ADD_MENU_TAGS": "Etiquetas", "BUTTON_SHARE_ALL": "Todos", "BUTTON_SYNC": "Sincronização (CardDAV)", - "SEND_TO_ALL_CONTACT_EMAILS": "Send to all contact emails" + "SEND_TO_ALL_CONTACT_EMAILS": "Enviar para todos os contactos" }, "COMPOSE": { "LINK_SHOW_INPUTS": "Mostrar todos os campos", @@ -255,7 +255,7 @@ "TITLE_UPDATE_IDENTITY": "Atualizar identidade?", "BUTTON_ADD_IDENTITY": "Adicionar", "BUTTON_UPDATE_IDENTITY": "Atualizar", - "LABEL_SIGNATURE_ADD": "Add\/Edit signature", + "LABEL_SIGNATURE_ADD": "Adicionar\/Editar assinatura", "LABEL_SIGNATURE_INSERT_BEFORE": "Nas respostas, inserir esta assinatura acima da mensagem citada" }, "POPUPS_CREATE_FOLDER": { @@ -455,7 +455,7 @@ "IMAGES_OPTION_WHITELIST": "Lista Branca ou perguntar", "IMAGES_OPTION_ALWAYS": "Mostrar sempre", "IMAGES_WHITELIST": "Lista Branca de imagens", - "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab" + "MESSAGE_POPUP_WINDOW": "Abrir em nova janela em vez de um separador" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Adicionar destinatários automaticamente à sua lista de endereços", From 0a6ac6613cd0b15c9cb1166d4bfcf41001caff9c Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 29 Jan 2024 11:22:19 +0100 Subject: [PATCH 034/540] Resolve #1409 --- .../RainLoop/Providers/AddressBook/CardDAV.php | 12 ++++++------ .../Providers/AddressBook/PdoAddressBook.php | 2 +- .../v/0.0.0/app/libraries/snappymail/dav/client.php | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/CardDAV.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/CardDAV.php index 2833a9422..6d572a5b1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/CardDAV.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/CardDAV.php @@ -187,7 +187,7 @@ trait CardDAV if (!$oClient) { return $aContactsPaths; } - $sNextPath = $oClient->__UrlPath__; + $sNextPath = $oClient->urlPath; } if ($sPath != $sNextPath) { @@ -219,7 +219,7 @@ trait CardDAV if (!$oClient) { return $aContactsPaths; } - $sCurrentUserPrincipal = $oClient->__UrlPath__; + $sCurrentUserPrincipal = $oClient->urlPath; } $aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal); @@ -243,7 +243,7 @@ trait CardDAV if (!$oClient) { return $aContactsPaths; } - $sAddressbookHomeSet = $oClient->__UrlPath__; + $sAddressbookHomeSet = $oClient->urlPath; } $aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet); @@ -287,7 +287,7 @@ trait CardDAV } } if ($bGood) { - $oClient->__UrlPath__ = $sPath; + $oClient->urlPath = $sPath; } } @@ -332,7 +332,7 @@ trait CardDAV $oClient = new DAVClient($aSettings); $oClient->setVerifyPeer(false); - $oClient->__UrlPath__ = $aUrl['path']; + $oClient->urlPath = $aUrl['path']; $this->logWrite('DavClient: User: '.$aSettings['userName'].', Url: '.$sUrl, \LOG_INFO, 'DAV'); @@ -361,7 +361,7 @@ trait CardDAV $oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy); - $sPath = $oClient->__UrlPath__; + $sPath = $oClient->urlPath; $bGood = true; if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath)) { 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 2c26073fd..c072478c1 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 @@ -135,7 +135,7 @@ class PdoAddressBook return false; } - $sPath = $oClient->__UrlPath__; + $sPath = $oClient->urlPath; $time = \microtime(true); $aRemoteSyncData = $this->prepareDavSyncData($oClient, $sPath); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php b/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php index 528f8015a..5578e3241 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php @@ -10,7 +10,7 @@ namespace SnappyMail\DAV; class Client { -// public $__UrlPath__; + public $urlPath; const NS_DAV = 'urn:DAV', From 070d10a1d0c52acb5bce392a0f6aaad0b5c8c1dc Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 29 Jan 2024 11:25:42 +0100 Subject: [PATCH 035/540] Resolve #1408 --- snappymail/v/0.0.0/static/apple-touch-icon.png | Bin 992 -> 1439 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/snappymail/v/0.0.0/static/apple-touch-icon.png b/snappymail/v/0.0.0/static/apple-touch-icon.png index 78ab30b38cad48bf4ef39d67c2b624a5b608b172..a0f4c3c3436b745d6d93c3373c5f90d746a4c77e 100644 GIT binary patch delta 1422 zcmV;91#$Y|2cHX&BYy>ENkl(eZEd-fr>Cc*zrP=8X=z^yfa5rHcXwlZd)uYFu&@w=gM&y& zNbs$Idxh%1SbsO!Qsk^z@W!YHHlgqyVT%1e!<>e)GI$cnw$ou8<5I>WYt# zr_<9@kKHypI{MS>aP$P#$jFGtmjCTCH#dju>}=rd?0<}!o0~(f2Xu6F(Dn5-sT2wY zj7B2>U}k2baY@~VBqbCV=Q>< z3DworWHy_L=lP&rpCSAE`&3$5>b2$H#lF7*!5f6y+FCj}Ir%Z`fB>9-0|0=GMk5i? zkB8vTd4D3J;o;%;_ul?67z|D&XJ=>B(9qzfq^_>+TSE}yJQ0z>VDJsV0img>>Bm42 z>bzh8zJ|khgAj=G{{rxt5lT!dz0;ye){IXM~g z^Yd=GN`*op$Sy%i(TIo$0D!?@!1VOA%e}_NMrgHKsMYFEb^h`35&ixBn4O(+y~Tik#cH(@%d&1`jb&M~TCJZp=B3&pI0ksy8auYEdcEFRuGi~vb#;Zz z%zw;JX?>H)gz@olmok=Rq0{L^^+drvz+Ve|)fH$DeBT2^eHn7}1eRsLd|NA}!0!cMu~@)y z9C)60mf7uga2$u*+gkvD&1M6~ac&$|Y8XAH|qj6DU zwOXOkXq*}4#JUl-?BPuEiIXO8OlapaKn-LuyEq`h`y}rKg_gLFU*yRyGY-}uIV`Kg1)EpZihG7sH z8R-(R0uiNBiPY3ozt4%a%hyppk3(R2c^R#(tvEP1@LKWV;Q{UK?HC#wlDrrZs!NPr zM@L6!ZEf{B4Jy1P95*&LJf6UOPkCo&2RAo2LCNk4(Iv(%p69W#u>q^qDt|mBX1Cjk z=Xsy!d59>eUweh<5@VOAI)0O)48sV@sVfx0+e!%0CB`m)b>z*E3_u1D+Tsj?ySuws zU0n?cr+0UE{|3qbr-RTP0$Qy$WS!S)wO}3}AL-)aA|wGMBqV^5|8Y{D24w&;02zP` cAavzFbnG&=V>I2F00000Ne4wvM6N<$f`O{H+W-In delta 971 zcmV;+12p`f3*ZNkBYy*@NklVULQ{$JpCnK?89D}3^&fPYC76>JBd13x|K6L7Es z8gUOW3jFb;VW7(d`PV?+lfDB371WdW17Cgo9El@BwUA#F5vq;6Q4y+@yb%$qoqQ}2 zg2?{>4g)EJDG{(Fo2=siFi}i|gp2%bV4II)-$WQwm;+7$&pi2^z)izFp#`Y-$sYv{ z0lBc2U#v7MU4PVzeIib&{1^eclEAfqmS5^v4_rwCXJyw^Yj_8oNXvfy2yhG7==0$u zFlKlTnDNQ~1!Ohi6BV=rO#$TlogdDu*dzOU?^qcJGQcu}DG?H!nM(cAcwRnlfezQ) zw_s@q65)$1E7gKNMe=!Ef8LeC)h&Wq+gx`0Qh3tinZ5A&+nk6~GZ; zxQawjC9f<%*l?&c5p>9F3E+tE0hsork8&DQZe%3z@%r~Ffcydh(h3!1fnC5VpL4+} z2|Q-YL_P&P0=D>^RnDsVRjda%SPcxyA*ee&2Aa!2zFF$_`ZFlLhCyoxcq`5NjJ0fu zMTqN%)_)LiV_YVjJ3zN9zgt@4eTf9vD%)H)hQzmVU1K^upjZo(E9v&Y|2-fXu?3u- zFjN7nK;)JONXDqR5=4ka9{jJnqKI(Da2TsPbT7prA7>7zcEp|omJF-_Rsbu2654^C z6dp>rSCAddAF0>NH{OHQR>^NDvKmFP6~`~-RevA>Jd{Agz5U!Ic~9h<0g1k@ddgpx z{G%ZPM6ngeFYQ<$C!z1OD{lsP!2Gq@1aKdC;VJu4^265asJ7zxW%mJL`+$?c4WL)U zXs47p9l@a{;5u-j$d1yuUqJ%&0>^>P8WpulnY|GNSP2}%bz4(Pzx;+khn%&o)ojHK zg?~#2T`Z^Y;;g60cc%O*dLoo0_#uNE&SL}=M#@~&nFAVtb<#D%RBi#*7;1eLTT%R~lQNAub3ls1EpFIOF(Kci4G5rW zD~ex9`JK|)2&tT@=P5i%HFdb5Vk?SYb3~GCFk}U=0$2fj<8TLUz_}XX-R530^0ht$ tay869myjLP?iv-~DHawM78Vw@i+@H#{u)yjc^?1(002ovPDHLkV1j;oyAA*V From 9eb5646aeccc5c800535dcf39c522ac48f9f704f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 30 Jan 2024 18:16:39 +0100 Subject: [PATCH 036/540] Redesign WYSIWYG handling to allow any editor (quill, tiny, etc. etc.) --- dev/Common/Html.js | 48 ++++------- dev/External/SquireUI.js | 27 +++--- dev/External/User/ko.js | 2 +- dev/Settings/User/General.js | 8 +- dev/Stores/User/Settings.js | 4 +- dev/View/Popup/Compose.js | 1 - plugins/wysiwyg-example/example.js | 82 +++++++++++++++++++ plugins/wysiwyg-example/index.php | 41 ++++++++++ plugins/wysiwyg-example/static/example.min.js | 1 + .../0.0.0/app/libraries/RainLoop/Actions.php | 2 + .../app/libraries/RainLoop/Actions/User.php | 1 + .../templates/Views/User/SettingsGeneral.html | 14 ++++ 12 files changed, 180 insertions(+), 51 deletions(-) create mode 100644 plugins/wysiwyg-example/example.js create mode 100644 plugins/wysiwyg-example/index.php create mode 100644 plugins/wysiwyg-example/static/example.min.js diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 063caaab7..b2d929afa 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -725,20 +725,12 @@ export const WYSIWYGS = ko.observableArray(); -WYSIWYGS.push(['Squire', (owner, container, onReady)=>{ - let squire = new SquireUI(container); - setTimeout(()=>onReady(squire), 1); -/* - squire.on('blur', () => owner.blurTrigger()); - squire.on('focus', () => clearTimeout(owner.blurTimer)); - squire.on('mode', () => { - owner.blurTrigger(); - owner.onModeChange?.(!owner.isPlain()); - }); -*/ -}]); +WYSIWYGS.push({ + name: 'Squire', + construct: (owner, container, onReady) => onReady(new SquireUI(container)) +}); -rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push([name, construct]); +rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct}); export class HtmlEditor { /** @@ -747,7 +739,7 @@ export class HtmlEditor { * @param {Function=} onReady * @param {Function=} onModeChange */ - constructor(element, onBlur = null, onReady = null, onModeChange = null) { + constructor(element, onReady = null, onModeChange = null, onBlur = null) { this.blurTimer = 0; this.onBlur = onBlur; @@ -757,10 +749,10 @@ export class HtmlEditor { onReady = onReady ? [onReady] : []; this.onReady = fn => onReady.push(fn); // TODO: make 'which' user configurable -// const which = 'CKEditor4', -// wysiwyg = WYSIWYGS.find(item => which == item[0]) || WYSIWYGS.find(item => 'Squire' == item[0]); - const wysiwyg = WYSIWYGS.find(item => 'Squire' == item[0]); - wysiwyg[1](this, element, editor => { + const which = SettingsUserStore.editorWysiwyg(), + wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name); +// const wysiwyg = WYSIWYGS.find(item => 'Squire' == item.name); + wysiwyg.construct(this, element, editor => setTimeout(()=>{ this.editor = editor; editor.on('blur', () => this.blurTrigger()); editor.on('focus', () => clearTimeout(this.blurTimer)); @@ -770,7 +762,7 @@ export class HtmlEditor { }); this.onReady = fn => fn(); onReady.forEach(fn => fn()); - }); + },1)); } } @@ -826,8 +818,8 @@ export class HtmlEditor { let result = ''; if (this.editor) { try { - if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) { - result = this.editor.__plain.getRawData(); + if (this.isPlain()) { + result = this.editor.getPlainData(); } else { result = this.editor.getData(); } @@ -862,8 +854,8 @@ export class HtmlEditor { this.clearCachedSignature(); try { editor.setMode(mode); - if (this.isPlain() && editor.plugins.plain && editor.__plain) { - editor.__plain.setRawData(data); + if (this.isPlain()) { + editor.setPlainData(data); } else { editor.setData(data); } @@ -883,16 +875,8 @@ export class HtmlEditor { this.onReady(() => this.editor.focus()); } - hasFocus() { - try { - return !!this.editor?.focusManager.hasFocus; - } catch (e) { - return false; - } - } - blur() { - this.onReady(() => this.editor.focusManager.blur(true)); + this.onReady(() => this.editor.blur()); } clear() { diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index 18d1ba0fd..015b9ece7 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -318,11 +318,6 @@ class SquireUI wysiwyg.className = 'squire-wysiwyg'; wysiwyg.dir = 'auto'; this.mode = ''; // 'plain' | 'wysiwyg' - this.__plain = { - getRawData: () => this.plain.value, - setRawData: plain => this.plain.value = plain - }; - this.container = container; this.squire = squire; this.plain = plain; @@ -530,15 +525,6 @@ class SquireUI console.dir({select:e.range}); }); */ - - // CKEditor gimmicks used by HtmlEditor - this.plugins = { - plain: true - }; - this.focusManager = { - hasFocus: () => squire._isFocused, - blur: () => squire.blur() - }; } doAction(name) { @@ -576,7 +562,6 @@ class SquireUI this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0; } - // CKeditor gimmicks used by HtmlEditor on(type, fn) { if ('mode' == type) { this.onModeChange = fn; @@ -643,6 +628,18 @@ class SquireUI squire.setSelection( range ); } + getPlainData() { + return this.plain.value; + } + + setPlainData(text) { + this.plain.value = text; + } + + blur() { + this.squire.blur(); + } + focus() { if ('plain' == this.mode) { this.plain.focus(); diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index 8fc26ffd5..306162bc6 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -91,7 +91,7 @@ Object.assign(ko.bindingHandlers, { }; if (ko.isObservable(fValue)) { - editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue); + editor = new HtmlEditor(element, fOnReady, fUpdateKoValue, fUpdateKoValue); fValue.__fetchEditorValue = fUpdateKoValue; diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js index ebb12c97d..3c2584c5b 100644 --- a/dev/Settings/User/General.js +++ b/dev/Settings/User/General.js @@ -5,6 +5,7 @@ import { SaveSettingStatus } from 'Common/Enums'; import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser'; import { setRefreshFoldersInterval } from 'Common/Folders'; import { Settings, SettingsGet } from 'Common/Globals'; +import { WYSIWYGS } from 'Common/Html'; import { isArray } from 'Common/Utils'; import { addSubscribablesTo, addComputablesTo } from 'External/ko'; import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator'; @@ -44,7 +45,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { ['useThreads', // These use addSetting() 'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', - 'editorDefaultType', 'msgDefaultAction', 'maxBlockquotesLevel', + 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', // These are in addSettings() 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave', @@ -59,6 +60,8 @@ export class UserSettingsGeneral extends AbstractViewSettings { this.identities = IdentityUserStore; + this.wysiwygs = WYSIWYGS; + addComputablesTo(this, { languageFullName: () => convertLangName(this.language()), @@ -80,6 +83,8 @@ export class UserSettingsGeneral extends AbstractViewSettings { ]; }, + hasWysiwygs: () => 1 < WYSIWYGS().length, + msgDefaultActions: () => { translateTrigger(); return [ @@ -99,6 +104,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { }); this.addSetting('EditorDefaultType'); + this.addSetting('editorWysiwyg'); this.addSetting('MsgDefaultAction'); this.addSetting('MessageReadDelay'); this.addSetting('MessagesPerPage'); diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js index 234690783..9c577715e 100644 --- a/dev/Stores/User/Settings.js +++ b/dev/Stores/User/Settings.js @@ -47,6 +47,7 @@ export const SettingsUserStore = new class { layout: 1, editorDefaultType: 'Html', + editorWysiwyg: 'Squire', msgDefaultAction: 1 }); @@ -78,9 +79,10 @@ export const SettingsUserStore = new class { init() { const self = this; - self.editorDefaultType(SettingsGet('EditorDefaultType')); [ + 'EditorDefaultType', + 'editorWysiwyg', 'messageNewWindow', 'messageReadAuto', 'MsgDefaultAction', diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 04cd423fc..ec4b3381d 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -704,7 +704,6 @@ export class ComposePopupView extends AbstractViewPopup { // setTimeout(() => { this.oEditor = new HtmlEditor( this.editorArea(), - null, () => fOnInit(this.oEditor), bHtml => this.isHtml(!!bHtml) ); diff --git a/plugins/wysiwyg-example/example.js b/plugins/wysiwyg-example/example.js new file mode 100644 index 000000000..a26fe5158 --- /dev/null +++ b/plugins/wysiwyg-example/example.js @@ -0,0 +1,82 @@ +(rl => { + class Example + { + constructor(owner, editor) { + this.mode = 'wysiwyg'; + this.owner = owner; + this.editor = editor; +console.dir({editor}); + } + + setMode(mode) { + console.log(`WYSIWYG-Example.setMode(${mode})`); + this.mode = mode; + } + + on(type, fn) { + console.log(`WYSIWYG-Example.on(${type}, ${fn})`); + } + + execCommand(cmd, cfg) { + console.log(`WYSIWYG-Example.execCommand(${cmd}, ${cfg})`); +/* + execCommand('insertSignature', { + clearCache: true + })); + + execCommand('insertSignature', { + isHtml: html, + insertBefore: insertBefore, + signature: signature + })); +*/ + } + + getData() { + console.log(`WYSIWYG-Example.getData()`); + return this.editor.innerHTML; + } + + setData(html) { + console.log(`WYSIWYG-Example.setData(${html})`); + this.editor.innerHTML = html; + } + + getPlainData() { + console.log(`WYSIWYG-Example.getPlainData()`); + return this.editor.innerText; + } + + setPlainData(text) { + console.log(`WYSIWYG-Example.setPlainData(${text})`); + return this.editor.textContent = text; + } + + blur() { + console.log(`WYSIWYG-Example.blur()`); + this.editor.blur(); + } + + focus() { + console.log(`WYSIWYG-Example.focus()`); + } + } + + if (rl) { + const path = rl.settings.app('webVersionPath'), + script = document.createElement('script'); + script.src = path + 'static/wysiwyg-example/example.min.js'; + document.head.append(script); + + /** + * owner = HtmlEditor + * container = HTMLElement + * onReady = callback(SMQuill) + */ + rl.registerWYSIWYG('Example', (owner, container, onReady) => { + const editor = new Example(owner, container); + onReady(editor); + }); + } + +})(window.rl); diff --git a/plugins/wysiwyg-example/index.php b/plugins/wysiwyg-example/index.php new file mode 100644 index 000000000..a2613ecbd --- /dev/null +++ b/plugins/wysiwyg-example/index.php @@ -0,0 +1,41 @@ +isDir()) { + \mkdir($path . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } else { + \copy($item, $path . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + } + umask($old_mask); + } + + if (\is_file("{$path}/example.min.js")) { +// $this->addCss('style.css'); + $this->addJs('example.js'); + } + } +} diff --git a/plugins/wysiwyg-example/static/example.min.js b/plugins/wysiwyg-example/static/example.min.js new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/plugins/wysiwyg-example/static/example.min.js @@ -0,0 +1 @@ + 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 10535e2d1..eebc01df7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -702,6 +702,7 @@ class Actions 'DesktopNotifications' => true, 'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW), 'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')), + 'editorWysiwyg' => 'Squire', 'UseCheckboxesInList' => (bool) $oConfig->Get('defaults', 'view_use_checkboxes', true), 'showNextMessage' => (bool) $oConfig->Get('defaults', 'view_show_next_message', false), 'AutoLogout' => (int) $oConfig->Get('defaults', 'autologout', 30), @@ -766,6 +767,7 @@ class Actions } $aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType'])); + $aResult['editorWysiwyg'] = $oSettings->GetConf('editorWysiwyg', $aResult['editorWysiwyg']); $aResult['requestReadReceipt'] = (bool) $oSettings->GetConf('requestReadReceipt', false); $aResult['requestDsn'] = (bool) $oSettings->GetConf('requestDsn', false); $aResult['requireTLS'] = (bool) $oSettings->GetConf('requireTLS', false); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php index a58b4e82e..2805c5222 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php @@ -177,6 +177,7 @@ trait User }); $this->setSettingsFromParams($oSettings, 'EditorDefaultType', 'string'); + $this->setSettingsFromParams($oSettings, 'editorWysiwyg', 'string'); $this->setSettingsFromParams($oSettings, 'requestReadReceipt', 'bool'); $this->setSettingsFromParams($oSettings, 'requestDsn', 'bool'); $this->setSettingsFromParams($oSettings, 'requireTLS', 'bool'); diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html index d18d11235..dc2bd1350 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html @@ -119,6 +119,20 @@ } }">
+
+ +
+
+
{ props && Object.entries(props).forEach(([k,v]) => { - if ("style" === k && typeof v === "object") { - Object.entries(v).forEach(([k,v]) => node.style[k] = v); - } else if (v != null) { - node.setAttribute(k, v); - } else { + if (null == v) { node.removeAttribute(k); + } else if ("style" === k && typeof v === "object") { + Object.entries(v).forEach(([k,v]) => node.style[k] = v); + } else { + node.setAttribute(k, v); } }); }, @@ -2262,7 +2262,7 @@ class Squire }, formatRemove: event => { event.preventDefault(); - this.setStyle(null); + this.setStyle(); }, formatSetBlockTextDirection: event => { event.preventDefault(); @@ -3665,9 +3665,20 @@ class Squire let end = range ? range.endContainer : 0; // When the selection is all the text inside an element, set style on the element itself if ("dir" == name || (isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset)) { - this.saveUndoState(range); + this._recordUndoState(range); setAttributes(start.parentNode, {[name]: value}); - this.setRange(range); +// this.setRange(range); + this._docWasChanged(); + } + // Else when it should remove the attribute + else if (null == value) { + this._recordUndoState(range); + let node = getClosest(range.commonAncestorContainer, this._root, '*'); + range.collapsed + ? setAttributes(node, {[name]: value}) + : node.querySelectorAll('*').forEach(el => setAttributes(el, {[name]: value})); +// this.setRange(range); + this._docWasChanged(); } // Else create a span element else { From 84aa997c6268cee365f1bfbd5a02ae8ec64510d5 Mon Sep 17 00:00:00 2001 From: tinola Date: Fri, 2 Feb 2024 08:26:02 +0100 Subject: [PATCH 038/540] Update Polish translation --- .../v/0.0.0/app/localization/pl-PL/admin.json | 2 +- snappymail/v/0.0.0/app/localization/pl-PL/user.json | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/snappymail/v/0.0.0/app/localization/pl-PL/admin.json b/snappymail/v/0.0.0/app/localization/pl-PL/admin.json index a214fe4f5..01c77606f 100644 --- a/snappymail/v/0.0.0/app/localization/pl-PL/admin.json +++ b/snappymail/v/0.0.0/app/localization/pl-PL/admin.json @@ -68,7 +68,7 @@ "LABEL_STORAGE_DSN": "Adres DSN", "LABEL_STORAGE_USER": "Użytkownik", "LABEL_STORAGE_PASSWORD": "Hasło", - "SQLITE_GLOBAL": "Use a global DB instead of per account", + "SQLITE_GLOBAL": "Używaj globalnej bazy kontaktów zamiast indywidualnych dla każdego konta.", "ALERT_NOTICE": "Ostrzeżenie!", "SUGGESTIONS_LIMIT": "Limit sugestii", "HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "Nie używaj tej bazy danych z dużą ilością aktywnych użytkowników.", diff --git a/snappymail/v/0.0.0/app/localization/pl-PL/user.json b/snappymail/v/0.0.0/app/localization/pl-PL/user.json index 30530d091..053d9f704 100644 --- a/snappymail/v/0.0.0/app/localization/pl-PL/user.json +++ b/snappymail/v/0.0.0/app/localization/pl-PL/user.json @@ -28,7 +28,7 @@ "REPLY_TO": "Odpisz do", "SAVE": "Zapisz", "SAVE_CHANGES": "Zapisać zmiany?", - "SEARCH": "Nie znaleziono żadnych kontaktów", + "SEARCH": "Szukaj", "SPAM": "Spam", "SUBJECT": "Temat", "TEST": "Testuj", @@ -178,7 +178,7 @@ "LABEL_ORGANIZATION": "Organizacja", "LABEL_DEPARTMENT": "Departament", "LABEL_TITLE": "Tytuł", - "TAB_CRYPTO": "Krypto", + "TAB_CRYPTO": "Kryptografia", "ASK": "Pytaj", "NEVER": "Nigdy", "ALWAYS": "Zawsze", @@ -210,7 +210,7 @@ "ADD_MENU_TAGS": "Tagi", "BUTTON_SHARE_ALL": "Wszystkim", "BUTTON_SYNC": "Synchronizacja (CardDAV)", - "SEND_TO_ALL_CONTACT_EMAILS": "Send to all contact emails" + "SEND_TO_ALL_CONTACT_EMAILS": "Wyślij na wszystkie adresy email dla tego kontaktu" }, "COMPOSE": { "LINK_SHOW_INPUTS": "Pokaż wszystkie pola", @@ -255,7 +255,7 @@ "TITLE_UPDATE_IDENTITY": "Czy zaktualizować tożsamość?", "BUTTON_ADD_IDENTITY": "Dodaj", "BUTTON_UPDATE_IDENTITY": "Aktualizuj", - "LABEL_SIGNATURE_ADD": "Add\/Edit signature", + "LABEL_SIGNATURE_ADD": "Dodaj\/Edytuj podpis", "LABEL_SIGNATURE_INSERT_BEFORE": "Umieszczaj ten podpis przed cytowanym tekstem w odpowiedziach" }, "POPUPS_CREATE_FOLDER": { @@ -386,8 +386,7 @@ "IMAGEURL": "Odnośnik obrazu", "IMAGEUPLOAD": "Wybierz obraz", "UNDO": "Cofnij", - "REDO": "Ponów", - "REMOVESTYLE": "Remove style" + "REDO": "Ponów" }, "SETTINGS_LABELS": { "GENERAL": "Ogólne", @@ -456,7 +455,7 @@ "IMAGES_OPTION_WHITELIST": "Dodaj do białej listy lub pytaj", "IMAGES_OPTION_ALWAYS": "Zawsze pokazuj", "IMAGES_WHITELIST": "Biała lista obrazów", - "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab" + "MESSAGE_POPUP_WINDOW": "Pokaż w nowym oknie zamiast w nowej karcie" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatycznie dodawaj odbiorców do książki adresowej", From f105ed3f9da21cd0d2a36720da3e64cc3e5ecb8e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sat, 3 Feb 2024 12:06:56 +0100 Subject: [PATCH 039/540] Changed Knockout a bit --- .../build/output/knockout-latest.debug.js | 132 +++++++++-------- .../knockout/build/output/knockout-latest.js | 135 +++++++++--------- .../src/binding/bindingAttributeSyntax.js | 2 +- .../src/binding/defaultBindings/checked.js | 4 +- .../src/binding/defaultBindings/event.js | 4 +- .../src/binding/defaultBindings/value.js | 2 +- .../knockout/src/components/loaderRegistry.js | 4 +- .../src/subscribables/dependencyDetection.js | 4 +- .../src/subscribables/dependentObservable.js | 40 +++--- .../knockout/src/subscribables/observable.js | 16 ++- .../observableArray.changeTracking.js | 4 +- .../src/subscribables/observableArray.js | 15 +- .../src/subscribables/subscribable.js | 33 ++--- vendors/knockout/src/templating/templating.js | 2 +- vendors/knockout/src/utils.domData.js | 2 +- 15 files changed, 205 insertions(+), 194 deletions(-) diff --git a/vendors/knockout/build/output/knockout-latest.debug.js b/vendors/knockout/build/output/knockout-latest.debug.js index 4c8b5e185..1cdf2b241 100644 --- a/vendors/knockout/build/output/knockout-latest.debug.js +++ b/vendors/knockout/build/output/knockout-latest.debug.js @@ -141,7 +141,7 @@ ko.utils.domData = { } return value; }, - getOrSet: function(node, key, value) { + getOrSet(node, key, value) { return this.get(node, key) || this.set(node, key, value); }, clear: node => dataStore.delete(node), @@ -308,19 +308,23 @@ class koSubscription ko.subscribable = function () { Object.setPrototypeOf(this, ko_subscribable_fn); - ko_subscribable_fn.init(this); + ko_subscribable_fn['init'](this); } var defaultEvent = "change"; +//const IS_SUBSCRIBABLE = Symbol('IS_SUBSCRIBABLE'); + var ko_subscribable_fn = { - init: instance => { +// [IS_SUBSCRIBABLE]: 1, + + 'init': instance => { instance._subscriptions = new Map(); instance._subscriptions.set("change", new Set); instance._versionNumber = 1; }, - subscribe: function (callback, callbackTarget, event) { + 'subscribe'(callback, callbackTarget, event) { var self = this; event = event || defaultEvent; @@ -339,7 +343,7 @@ var ko_subscribable_fn = { return subscription; }, - notifySubscribers: function (valueToNotify, event) { + notifySubscribers(valueToNotify, event) { event = event || defaultEvent; if (event === defaultEvent) { this.updateVersion(); @@ -359,19 +363,19 @@ var ko_subscribable_fn = { } }, - getVersion: function () { + getVersion() { return this._versionNumber; }, - hasChanged: function (versionToCheck) { + hasChanged(versionToCheck) { return this.getVersion() !== versionToCheck; }, - updateVersion: function () { + updateVersion() { ++this._versionNumber; }, - limit: function(limitFunction) { + limit(limitFunction) { var self = this, selfIsObservable = ko.isObservable(self), ignoreBeforeChange, notifyNextChange, previousValue, pendingValue, didUpdate, beforeChange = 'beforeChange'; @@ -432,17 +436,17 @@ var ko_subscribable_fn = { }; }, - hasSubscriptionsForEvent: function(event) { + hasSubscriptionsForEvent(event) { return (this._subscriptions.get(event) || []).size; }, - isDifferent: function(oldValue, newValue) { + isDifferent(oldValue, newValue) { return !this.equalityComparer || !this.equalityComparer(oldValue, newValue); }, toString: () => '[object Object]', - extend: function(requestedExtenders) { + 'extend'(requestedExtenders) { var target = this; if (requestedExtenders) { ko.utils.objectForEach(requestedExtenders, (key, value) => { @@ -456,20 +460,17 @@ var ko_subscribable_fn = { } }; -ko.exportProperty(ko_subscribable_fn, 'init', ko_subscribable_fn.init); -ko.exportProperty(ko_subscribable_fn, 'subscribe', ko_subscribable_fn.subscribe); -ko.exportProperty(ko_subscribable_fn, 'extend', ko_subscribable_fn.extend); - // For browsers that support proto assignment, we overwrite the prototype of each // observable instance. Since observables are functions, we need Function.prototype // to still be in the prototype chain. ko.subscribable['fn'] = Object.setPrototypeOf(ko_subscribable_fn, Function.prototype); +//ko.isSubscribable = obj => !!(obj && obj[IS_SUBSCRIBABLE]); ko.isSubscribable = instance => - typeof instance?.subscribe == "function" && typeof instance.notifySubscribers == "function"; + typeof instance?.['subscribe'] == "function" && typeof instance.notifySubscribers == "function"; (() => { -var outerFrames = [], +let outerFrames = [], currentFrame, lastId = 0, @@ -494,7 +495,7 @@ ko.dependencyDetection = { } }, - ignore: (callback, callbackTarget, callbackArgs) => { + ignore(callback, callbackTarget, callbackArgs) { try { begin(); return callback.apply(callbackTarget, callbackArgs || []); @@ -513,6 +514,7 @@ ko.dependencyDetection = { })(); const observableLatestValue = Symbol('_latestValue'), length = 'length'; +//const IS_OBSERVABLE = Symbol('IS_OBSERVABLE'); ko.observable = initialValue => { function observable() { @@ -523,7 +525,7 @@ ko.observable = initialValue => { if (observable.isDifferent(observable[observableLatestValue], arguments[0])) { observable.valueWillMutate(); observable[observableLatestValue] = arguments[0]; - observable.valueHasMutated(); + observable['valueHasMutated'](); } return this; // Permits chained assignments } @@ -539,7 +541,7 @@ ko.observable = initialValue => { }); // Inherit from 'subscribable' - ko.subscribable['fn'].init(observable); + ko.subscribable['fn']['init'](observable); // Inherit from 'observable' return Object.setPrototypeOf(observable, observableFn); @@ -547,17 +549,18 @@ ko.observable = initialValue => { // Define prototype for observables var observableFn = { - 'toJSON': function() { +// [IS_OBSERVABLE]: 1, + 'toJSON'() { let value = this[observableLatestValue]; return value?.toJSON?.() || value; }, equalityComparer: valuesArePrimitiveAndEqual, - peek: function() { return this[observableLatestValue]; }, - valueHasMutated: function () { + peek() { return this[observableLatestValue]; }, + 'valueHasMutated'() { this.notifySubscribers(this[observableLatestValue], 'spectate'); this.notifySubscribers(this[observableLatestValue]); }, - valueWillMutate: function () { this.notifySubscribers(this[observableLatestValue], 'beforeChange'); } + valueWillMutate() { this.notifySubscribers(this[observableLatestValue], 'beforeChange'); } }; // Note that for browsers that don't support proto assignment, the @@ -567,6 +570,7 @@ Object.setPrototypeOf(observableFn, ko.subscribable['fn']); var protoProperty = ko.observable.protoProperty = '__ko_proto__'; observableFn[protoProperty] = ko.observable; +//ko.isObservable = obj => !!(obj && obj[IS_OBSERVABLE]); ko.isObservable = instance => { var proto = typeof instance == 'function' && instance[protoProperty]; if (proto && proto !== observableFn[protoProperty] && proto !== ko.computed['fn'][protoProperty]) { @@ -584,7 +588,6 @@ ko.isWriteableObservable = instance => { ko.exportSymbol('observable', ko.observable); ko.exportSymbol('isObservable', ko.isObservable); ko.exportSymbol('observable.fn', observableFn); -ko.exportProperty(observableFn, 'valueHasMutated', observableFn.valueHasMutated); ko.observableArray = initialValues => { initialValues = initialValues || []; @@ -594,10 +597,13 @@ ko.observableArray = initialValues => { return Object.setPrototypeOf(ko.observable(initialValues), ko.observableArray['fn']).extend({'trackArrayChanges':true}); }; +//const IS_OBSERVABLE_ARRAY = Symbol('IS_OBSERVABLE_ARRAY'); + // Note that for browsers that don't support proto assignment, the // inheritance chain is created manually in the ko.observableArray constructor ko.observableArray['fn'] = Object.setPrototypeOf({ - 'remove': function (valueOrPredicate) { +// [IS_OBSERVABLE_ARRAY]: 1, + 'remove'(valueOrPredicate) { var underlyingArray = this.peek(); var removed = false; var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) @@ -614,7 +620,7 @@ ko.observableArray['fn'] = Object.setPrototypeOf({ underlyingArray.splice(i, 1); } } - removed && this.valueHasMutated(); + removed && this['valueHasMutated'](); } }, ko.observable['fn']); @@ -634,7 +640,7 @@ Object.getOwnPropertyNames(Array.prototype).forEach(methodName => { this.valueWillMutate(); this.cacheDiffForKnownOperation(underlyingArray, methodName, args); var methodCallResult = underlyingArray[methodName](...args); - this.valueHasMutated(); + this['valueHasMutated'](); // The native sort and reverse methods return a reference to the array, but it makes more sense to return the observable array instead. return methodCallResult === underlyingArray ? this : methodCallResult; }; @@ -647,11 +653,11 @@ Object.getOwnPropertyNames(Array.prototype).forEach(methodName => { } }); -ko.isObservableArray = instance => { - return ko.isObservable(instance) +//ko.isObservableArray = obj => !!(obj && obj[IS_OBSERVABLE_ARRAY]); +ko.isObservableArray = instance => + ko.isObservable(instance) && typeof instance["remove"] == "function" && typeof instance["push"] == "function"; -}; ko.exportSymbol('observableArray', ko.observableArray); ko.exportSymbol('isObservableArray', ko.isObservableArray); @@ -729,13 +735,13 @@ ko.extenders['trackArrayChanges'] = (target, options) => { trackingChanges = true; // Track how many times the array actually changed value - spectateSubscription = target.subscribe(() => ++pendingChanges, null, "spectate"); + spectateSubscription = target['subscribe'](() => ++pendingChanges, null, "spectate"); // Each time the array changes value, capture a clone so that on the next // change it's possible to produce a diff previousContents = [].concat(target.peek() || []); cachedDiff = null; - changeSubscription = target.subscribe(notifyChanges); + changeSubscription = target['subscribe'](notifyChanges); } function getChanges(previousContents, currentContents) { @@ -859,7 +865,7 @@ ko.computed = (evaluatorFunctionOrOptions, options) => { computedObservable.hasWriteFunction = typeof writeFunction === "function"; // Inherit from 'subscribable' - ko.subscribable['fn'].init(computedObservable); + ko.subscribable['fn']['init'](computedObservable); // Inherit from 'computed' Object.setPrototypeOf(computedObservable, computedFn); @@ -948,10 +954,10 @@ function evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyD var computedFn = { equalityComparer: valuesArePrimitiveAndEqual, - getDependenciesCount: function () { + getDependenciesCount() { return this[computedState].dependenciesCount; }, - getDependencies: function () { + getDependencies() { var dependencyTracking = this[computedState].dependencyTracking, dependentObservables = []; ko.utils.objectForEach(dependencyTracking, (id, dependency) => @@ -960,7 +966,7 @@ var computedFn = { return dependentObservables; }, - hasAncestorDependency: function (obs) { + hasAncestorDependency(obs) { if (!this[computedState].dependenciesCount) { return false; } @@ -968,7 +974,7 @@ var computedFn = { return dependencies.includes(obs) || !!dependencies.find(dep => dep.hasAncestorDependency && dep.hasAncestorDependency(obs)); }, - addDependencyTracking: function (id, target, trackingObj) { + addDependencyTracking(id, target, trackingObj) { if (this[computedState].pure && target === this) { throw Error("A 'pure' computed must not be called recursively"); } @@ -977,7 +983,7 @@ var computedFn = { trackingObj._order = this[computedState].dependenciesCount++; trackingObj._version = target.getVersion(); }, - haveDependenciesChanged: function () { + haveDependenciesChanged() { var id, dependency, dependencyTracking = this[computedState].dependencyTracking; for (id in dependencyTracking) { if (Object.prototype.hasOwnProperty.call(dependencyTracking, id)) { @@ -988,17 +994,17 @@ var computedFn = { } } }, - markDirty: function () { + markDirty() { // Process "dirty" events if we can handle delayed notifications if (!this[computedState].isBeingEvaluated) { this._evalDelayed?.(false /*isChange*/); } }, - isActive: function () { + isActive() { var state = this[computedState]; return state.isDirty || state.dependenciesCount > 0; }, - respondToChange: function () { + respondToChange() { // Ignore "change" events if we've already scheduled a delayed notification if (!this._notificationIsPending) { this.evaluatePossiblyAsync(); @@ -1006,10 +1012,10 @@ var computedFn = { this[computedState].isStale = true; } }, - subscribeToDependency: function (target) { - return target.subscribe(this.evaluatePossiblyAsync, this); + subscribeToDependency(target) { + return target['subscribe'](this.evaluatePossiblyAsync, this); }, - evaluatePossiblyAsync: function () { + evaluatePossiblyAsync() { var computedObservable = this, throttleEvaluationTimeout = computedObservable['throttleEvaluation']; if (throttleEvaluationTimeout >= 0) { @@ -1023,7 +1029,7 @@ var computedFn = { computedObservable.evaluateImmediate(true /*notifyChange*/); } }, - evaluateImmediate: function (notifyChange) { + evaluateImmediate(notifyChange) { var computedObservable = this, state = computedObservable[computedState], disposeWhen = state.disposeWhen, @@ -1059,7 +1065,7 @@ var computedFn = { return changed; }, - evaluateImmediate_CallReadWithDependencyDetection: function (notifyChange) { + evaluateImmediate_CallReadWithDependencyDetection(notifyChange) { // This function is really just part of the evaluateImmediate logic. You would never call it from anywhere else. // Factoring it out into a separate function means it can be independent of the try/catch block in evaluateImmediate, // which contributes to saving about 40% off the CPU overhead of computed evaluation (on V8 at least). @@ -1121,7 +1127,7 @@ var computedFn = { return changed; }, - peek: function (evaluate) { + peek(evaluate) { // By default, peek won't re-evaluate, except while the computed is sleeping. // Pass in true to evaluate if needed. var state = this[computedState]; @@ -1130,7 +1136,7 @@ var computedFn = { } return state.latestValue; }, - limit: function (limitFunction) { + limit(limitFunction) { var self = this; // Override the limit function with one that delays evaluation as well ko.subscribable['fn'].limit.call(self, limitFunction); @@ -1158,7 +1164,7 @@ var computedFn = { self._limitChange(self, !isChange /* isDirty */); }; }, - dispose: function () { + dispose() { var state = this[computedState]; if (!state.isSleeping && state.dependencyTracking) { ko.utils.objectForEach(state.dependencyTracking, (id, dependency) => @@ -1181,7 +1187,7 @@ var computedFn = { }; var pureComputedOverrides = { - beforeSubscriptionAdd: function (event) { + beforeSubscriptionAdd(event) { // If asleep, wake up the computed by subscribing to any dependencies. var computedObservable = this, state = computedObservable[computedState]; @@ -1220,7 +1226,7 @@ var pureComputedOverrides = { } } }, - afterSubscriptionRemove: function (event) { + afterSubscriptionRemove(event) { var state = this[computedState]; if (!state.isDisposed && event == 'change' && !this.hasSubscriptionsForEvent('change')) { ko.utils.objectForEach(state.dependencyTracking, (id, dependency) => { @@ -1237,7 +1243,7 @@ var pureComputedOverrides = { this.notifySubscribers(undefined, "asleep"); } }, - getVersion: function () { + getVersion() { // Because a pure computed is not automatically updated while it is sleeping, we can't // simply return the version number. Instead, we check if any of the dependencies have // changed and conditionally re-evaluate the computed observable. @@ -1878,7 +1884,7 @@ ko.bindingEvent = { if (options?.['notifyImmediately'] && bindingInfo.notifiedEvents[event]) { ko.dependencyDetection.ignore(callback, context, [node]); } - return bindingInfo.eventSubscribable.subscribe(callback, context, event); + return bindingInfo.eventSubscribable['subscribe'](callback, context, event); }, notify: (node, event) => { @@ -2169,11 +2175,11 @@ ko.exportSymbol('dataFor', ko.dataFor); // Join the loading process that is already underway, or start a new one. var subscribable = loadingSubscribablesCache[componentName]; if (subscribable) { - subscribable.subscribe(callback); + subscribable['subscribe'](callback); } else { // It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache. subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable(); - subscribable.subscribe(callback); + subscribable['subscribe'](callback); loadComponent(componentName, definition => { loadedDefinitionsCache.set(componentName, definition); @@ -2380,7 +2386,7 @@ ko.bindingHandlers['attr'] = { ko.bindingHandlers['checked'] = { 'after': ['value', 'attr'], - 'init': function (element, valueAccessor, allBindings) { + 'init'(element, valueAccessor, allBindings) { var isCheckbox = element.type == "checkbox", isRadio = element.type == "radio"; @@ -2494,7 +2500,7 @@ ko.bindingHandlers['checked'] = { //ko.expressionRewriting.twoWayBindings['checked'] = true; ko.bindingHandlers['checkedValue'] = { - 'update': function (element, valueAccessor) { + 'update'(element, valueAccessor) { element.value = ko.utils.unwrapObservable(valueAccessor()); } }; @@ -2540,7 +2546,7 @@ ko.bindingHandlers['disable'] = { // e.g. click:handler instead of the usual full-length event:{click:handler} function makeEventHandlerShortcut(eventName) { ko.bindingHandlers[eventName] = { - 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) { + 'init'(element, valueAccessor, allBindings, viewModel, bindingContext) { return ko.bindingHandlers['event']['init'].call(this, element, () => ({[eventName]: valueAccessor()}), // newValueAccessor allBindings, viewModel, bindingContext); @@ -2549,7 +2555,7 @@ function makeEventHandlerShortcut(eventName) { } ko.bindingHandlers['event'] = { - 'init' : (element, valueAccessor, allBindings, viewModel, bindingContext) => { + 'init'(element, valueAccessor, allBindings, viewModel, bindingContext) { ko.utils.objectForEach(valueAccessor() || {}, eventName => { if (typeof eventName == "string") { element.addEventListener(eventName, (...args) => { @@ -3092,7 +3098,7 @@ ko.bindingHandlers['value'] = { if (isSelectElement) { var updateFromModelComputed; - ko.bindingEvent.subscribe(element, ko.bindingEvent.childrenComplete, () => { + ko.bindingEvent['subscribe'](element, ko.bindingEvent.childrenComplete, () => { if (!updateFromModelComputed) { registerEventHandler("change", valueUpdateHandler); updateFromModelComputed = ko.computed(updateFromModel, { disposeWhenNodeIsRemoved: element }); @@ -3355,7 +3361,7 @@ makeEventHandlerShortcut('click'); if (!options['beforeRemove'] && ko.isObservableArray(arrayOrObservableArray)) { setDomNodeChildrenFromArrayMapping(arrayOrObservableArray.peek()); - var subscription = arrayOrObservableArray.subscribe(changeList => { + var subscription = arrayOrObservableArray['subscribe'](changeList => { setDomNodeChildrenFromArrayMapping(arrayOrObservableArray(), changeList); }, null, "arrayChange"); subscription.disposeWhenNodeIsRemoved(targetNode); diff --git a/vendors/knockout/build/output/knockout-latest.js b/vendors/knockout/build/output/knockout-latest.js index d4bc986fe..715e09c41 100644 --- a/vendors/knockout/build/output/knockout-latest.js +++ b/vendors/knockout/build/output/knockout-latest.js @@ -4,71 +4,70 @@ * License: MIT (http://www.opensource.org/licenses/mit-license.php) */ -(V=>{function P(a,b){return null===a||da[typeof a]?a===b:!1}function ea(a,b){var d;return()=>{d||(d=setTimeout(()=>{d=0;a()},b))}}function fa(a,b){var d;return()=>{clearTimeout(d);d=setTimeout(a,b)}}function ha(a,b){b?.A?.()}function ia(a,b){var d=this.$b,f=d[x];f.$||(this.Ya&&this.za[b]?(d.ub(b,a,this.za[b]),this.za[b]=null,--this.Ya):f.B[b]||d.ub(b,a,f.C?{X:a}:d.Sb(a)),a.ja&&a.Wb())}function W(a){a=c.g.j.get(a,K);var b=a?.H;b&&(a.H=null,b.Mb())}function X(a,b){for(var d,f=c.m.firstChild(b);d=f;)f= -c.m.nextSibling(d),Y(a,d);c.l.notify(b,c.l.G)}function Y(a,b){var d=a;if(1===b.nodeType||c.zb.mc(b))d=Z(b,null,a).bindingContextForDescendants;d&&!b.matches?.("SCRIPT,TEXTAREA,TEMPLATE")&&X(d,b)}function ja(a){var b=[],d={},f=[];c.g.P(a,function e(l){if(!d[l]){var g=c.h[l];g&&(g.after&&(f.push(l),g.after.forEach(k=>{if(a[k]){if(f.includes(k))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+f.join(", "));e(k)}}),f.length--),b.push({key:l,Hb:g}));d[l]=!0}}); -return b}function Z(a,b,d){var f=c.g.j.eb(a,K,{}),h=f.Xb;if(!b){if(h)throw Error("You cannot apply bindings multiple times to the same element.");f.Xb=!0}h||(f.context=d);f.gb||(f.gb={});if(b&&"function"!==typeof b)var l=b;else{var e=c.s(()=>{if(l=b?b(d,a):c.zb.cc(a,d))d[L]?.(),d[aa]?.();return l},{o:a});l&&e.isActive()||(e=null)}var g=d,k;if(l){var n=e?m=>()=>e()[m]():m=>l[m],p={get:m=>l[m]&&n(m)(),has:m=>m in l};c.l.G in l&&c.l.subscribe(a,c.l.G,()=>{var m=l[c.l.G]();if(m){var q=c.m.childNodes(a); -q.length&&m(q,c.Db(q[0]))}});c.l.ga in l&&(g=c.l.lb(a,d),c.l.subscribe(a,c.l.ga,()=>{var m=l[c.l.ga]();m&&c.m.firstChild(a)&&m(a)}));ja(l).forEach(m=>{var q=m.Hb.init,r=m.Hb.update,t=m.key;if(8===a.nodeType&&!c.m.ca[t])throw Error("The binding '"+t+"' cannot be used with virtual elements");try{"function"==typeof q&&c.u.M(()=>{var u=q(a,n(t),p,g.$data,g);if(u&&u.controlsDescendantBindings){if(void 0!==k)throw Error("Multiple bindings ("+k+" and "+t+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element."); -k=t}}),"function"==typeof r&&c.s(()=>r(a,n(t),p,g.$data,g),{o:a})}catch(u){throw u.message='Unable to process binding "'+t+": "+l[t]+'"\nMessage: '+u.message,u;}})}f=void 0===k;return{shouldBindDescendants:f,bindingContextForDescendants:f&&g}}function Q(a,b){return a&&a instanceof c.ea?a:new c.ea(a,void 0,void 0,b)}var M=V.document,R={},c="undefined"!==typeof R?R:{};c.v=(a,b)=>{a=a.split(".");for(var d=c,f=0,h=a.length-1;f{a[b]=d};c.v("version","3.5.1-sm"); -c.g={extend:(a,b)=>b?Object.assign(a,b):a,P:(a,b)=>a&&Object.entries(a).forEach(d=>b(d[0],d[1])),bb:a=>[...a.childNodes].forEach(b=>c.removeNode(b)),Lb:a=>{a=[...a];var b=(a[0]?.ownerDocument||M).createElement("div");a.forEach(d=>b.append(c.fa(d)));return b},ya:(a,b)=>Array.prototype.map.call(a,b?d=>c.fa(d.cloneNode(!0)):d=>d.cloneNode(!0)),ua:(a,b)=>{c.g.bb(a);b&&a.append(...b)},Ba:(a,b)=>{if(a.length){for(b=8===b.nodeType&&b.parentNode||b;a.length&&a[0].parentNode!==b;)a.splice(0,1);for(;1null==a?"":a.trim?a.trim():a.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,""),ab:a=>a.ownerDocument.documentElement.contains(1!==a.nodeType?a.parentNode:a),Tb:(a,b)=>{if(!a?.nodeType)throw Error("element must be a DOM node when calling triggerEvent");a.dispatchEvent(new Event(b))},i:a=>c.T(a)?a():a,kb:(a,b)=>a.textContent=c.g.i(b)||""};c.v("utils", -c.g);c.v("unwrap",c.g.i);(()=>{let a=0,b="__ko__"+Date.now(),d=new WeakMap;c.g.j={get:(f,h)=>(d.get(f)||{})[h],set:(f,h,l)=>{d.has(f)?d.get(f)[h]=l:d.set(f,{[h]:l});return l},eb:function(f,h,l){return this.get(f,h)||this.set(f,h,l)},clear:f=>d.delete(f),Z:()=>a++ +b}})();c.g.K=(()=>{var a=c.g.j.Z(),b={1:1,8:1,9:1},d={1:1,9:1};const f=(e,g)=>{var k=c.g.j.get(e,a);g&&!k&&(k=new Set,c.g.j.set(e,a,k));return k},h=e=>{var g=f(e);g&&(new Set(g)).forEach(k=>k(e));c.g.j.clear(e);d[e.nodeType]&&l(e.childNodes, -!0)},l=(e,g)=>{for(var k=[],n,p=0;p{if("function"!=typeof g)throw Error("Callback must be a function");f(e,1).add(g)},jb:(e,g)=>{var k=f(e);k&&(k.delete(g),k.size||c.g.j.set(e,a,null))},fa:e=>{c.u.M(()=>{b[e.nodeType]&&(h(e),d[e.nodeType]&&l(e.getElementsByTagName("*")))});return e},removeNode:e=>{c.fa(e);e.parentNode&&e.parentNode.removeChild(e)}}})();c.fa=c.g.K.fa;c.removeNode= -c.g.K.removeNode;c.v("utils.domNodeDisposal",c.g.K);c.v("utils.domNodeDisposal.addDisposeCallback",c.g.K.ma);c.cb={debounce:(a,b)=>a.Ja(d=>fa(d,b)),rateLimit:(a,b)=>a.Ja(d=>ea(d,b)),notify:(a,b)=>{a.pa="always"==b?null:P}};var da={undefined:1,"boolean":1,number:1,string:1};c.v("extenders",c.cb);class ka{constructor(a,b,d){this.X=a;this.ob=b;this.Da=d;this.Qa=!1;this.L=this.ia=null;c.ha(this,"dispose",this.A)}A(){this.Qa||(this.L&&c.g.K.jb(this.ia,this.L),this.Qa=!0,this.Da(),this.X=this.ob=this.Da= -this.ia=this.L=null)}o(a){this.ia=a;c.g.K.ma(a,this.L=this.A.bind(this))}}c.V=function(){Object.setPrototypeOf(this,D);D.Fa(this)};var D={Fa:a=>{a.W=new Map;a.W.set("change",new Set);a.tb=1},subscribe:function(a,b,d){var f=this;d=d||"change";var h=new ka(f,b?a.bind(b):a,()=>{f.W.get(d).delete(h);f.Ra?.(d)});f.Ta?.(d);f.W.has(d)||f.W.set(d,new Set);f.W.get(d).add(h);return h},D:function(a,b){b=b||"change";"change"===b&&this.Ma();if(this.ra(b)){b="change"===b&&this.Ub||new Set(this.W.get(b));try{c.u.yb(), -b.forEach(d=>{d.Qa||d.ob(a)})}finally{c.u.end()}}},Ca:function(){return this.tb},fc:function(a){return this.Ca()!==a},Ma:function(){++this.tb},Ja:function(a){var b=this,d=c.T(b),f,h,l,e,g;b.wa||(b.wa=b.D,b.D=(n,p)=>{p&&"change"!==p?"beforeChange"===p?b.qb(n):b.wa(n,p):b.rb(n)});var k=a(()=>{b.ja=!1;d&&e===b&&(e=b.pb?b.pb():b());var n=h||g&&b.Ha(l,e);g=h=f=!1;n&&b.wa(l=e)});b.rb=(n,p)=>{p&&b.ja||(g=!p);b.Ub=new Set(b.W.get("change"));b.ja=f=!0;e=n;k()};b.qb=n=>{f||(l=n,b.wa(n,"beforeChange"))};b.sb= -()=>{g=!0};b.Wb=()=>{b.Ha(l,b.I(!0))&&(h=!0)}},ra:function(a){return(this.W.get(a)||[]).size},Ha:function(a,b){return!this.pa||!this.pa(a,b)},toString:()=>"[object Object]",extend:function(a){var b=this;a&&c.g.P(a,(d,f)=>{d=c.cb[d];"function"==typeof d&&(b=d(b,f)||b)});return b}};c.ha(D,"init",D.Fa);c.ha(D,"subscribe",D.subscribe);c.ha(D,"extend",D.extend);c.V.fn=Object.setPrototypeOf(D,Function.prototype);c.kc=a=>"function"==typeof a?.subscribe&&"function"==typeof a.D;(()=>{var a=[],b,d=0;c.u={yb:f=> -{a.push(b);b=f},end:()=>b=a.pop(),Pb:f=>{if(b){if(!c.kc(f))throw Error("Only subscribable things can act as dependencies");b.Yb.call(b.Zb,f,f.Vb||(f.Vb=++d))}},M:(f,h,l)=>{try{return a.push(b),b=void 0,f.apply(h,l||[])}finally{b=a.pop()}},qa:()=>b?.s.qa(),Ia:()=>b?.Ia,s:()=>b?.s}})();const B=Symbol("_latestValue");c.ba=a=>{function b(){if(0 -null==b[B]?void 0:b[B].length});c.V.fn.Fa(b);return Object.setPrototypeOf(b,F)};var F={toJSON:function(){let a=this[B];return a?.toJSON?.()||a},pa:P,I:function(){return this[B]},Na:function(){this.D(this[B],"spectate");this.D(this[B])},nb:function(){this.D(this[B],"beforeChange")}};Object.setPrototypeOf(F,c.V.fn);var G=c.ba.pc="__ko_proto__";F[G]=c.ba;c.T=a=>{if((a="function"==typeof a&&a[G])&&a!==F[G]&&a!==c.s.fn[G])throw Error("Invalid object that looks like an observable; possibly from another Knockout instance"); -return!!a};c.Jb=a=>"function"==typeof a&&(a[G]===F[G]||a[G]===c.s.fn[G]&&a.hc);c.v("observable",c.ba);c.v("isObservable",c.T);c.v("observable.fn",F);c.ha(F,"valueHasMutated",F.Na);c.ta=a=>{a=a||[];if("object"!=typeof a||!("length"in a))throw Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");return Object.setPrototypeOf(c.ba(a),c.ta.fn).extend({trackArrayChanges:!0})};c.ta.fn=Object.setPrototypeOf({remove:function(a){for(var b=this.I(),d=!1, -f="function"!=typeof a||c.T(a)?e=>e===a:a,h=b.length;h--;){var l=b[h];if(f(l)){if(b[h]!==l)throw Error("Array modified during remove; cannot remove item");d||this.nb();d=!0;b.splice(h,1)}}d&&this.Na()}},c.ba.fn);Object.getOwnPropertyNames(Array.prototype).forEach(a=>{"function"===typeof Array.prototype[a]&&"constructor"!=a&&("copyWithin fill pop push reverse shift sort splice unshift".split(" ").includes(a)?c.ta.fn[a]=function(...b){var d=this.I();this.nb();this.Ab(d,a,b);b=d[a](...b);this.Na();return b=== -d?this:b}:c.ta.fn[a]=function(...b){return this()[a](...b)})});c.Ib=a=>c.T(a)&&"function"==typeof a.remove&&"function"==typeof a.push;c.v("observableArray",c.ta);c.v("isObservableArray",c.Ib);c.cb.trackArrayChanges=(a,b)=>{function d(){function m(){if(g){var q=[].concat(a.I()||[]);if(a.ra("arrayChange")){if(!h||1++g,null,"spectate"),k=[].concat(a.I()||[]),h=null,l=a.subscribe(m))}a.Va={};"object"== -typeof b&&c.g.extend(a.Va,b);a.Va.sparse=!0;if(!a.Ab){var f=!1,h=null,l,e,g=0,k,n=a.Ta,p=a.Ra;a.Ta=m=>{n?.call(a,m);"arrayChange"===m&&d()};a.Ra=m=>{p?.call(a,m);"arrayChange"!==m||a.ra("arrayChange")||(l?.A(),e?.A(),e=l=null,f=!1,k=void 0)};a.Ab=(m,q,r)=>{function t(A,v,I){return u[u.length]={status:A,value:v,index:I}}if(f&&!g){var u=[],y=m.length,w=r.length,z=0;switch(q){case "push":z=y;case "unshift":for(m=0;mr[0]?y+r[0]:r[0]),y);y=1===w?y:Math.min(z+(r[1]||0),y);w=z+w-2;q=Math.max(y,w);for(var C=[],H=[],O=2;z{function d(){if(0{d.A()});return d};var J={pa:P,qa:function(){return this[x].J},dc:function(){var a=[];c.g.P(this[x].B,(b,d)=>a[d.ka]=d.X);return a},fb:function(a){if(!this[x].J)return!1;var b=this.dc();return b.includes(a)||!!b.find(d=>d.fb&&d.fb(a))},ub:function(a,b,d){if(this[x].ib&&b===this)throw Error("A 'pure' computed must not be called recursively");this[x].B[a]=d;d.ka=this[x].J++;d.la=b.Ca()},sa:function(){var a, -b=this[x].B;for(a in b)if(Object.prototype.hasOwnProperty.call(b,a)){var d=b[a];if(this.va&&d.X.ja||d.X.fc(d.la))return!0}},uc:function(){this[x].Ga||this.va?.(!1)},isActive:function(){var a=this[x];return a.Y||0a.U(!0),b)):a.va?a.va(!0):a.U(!0)},U:function(a){var b=this[x],d=b.na, -f=!1;if(!b.Ga&&!b.$){if(b.o&&!c.g.ab(b.o)||d?.()){if(!b.mb){this.A();return}}else b.mb=!1;try{b.Ga=!0,f=this.bc(a)}finally{b.Ga=!1}return f}},bc:function(a){var b=this[x],d=b.ib?void 0:!b.J;var f={$b:this,za:b.B,Ya:b.J};c.u.yb({Zb:f,Yb:ia,s:this,Ia:d});b.B={};b.J=0;a:{try{var h=b.Ob();break a}finally{c.u.end(),f.Ya&&!b.C&&c.g.P(f.za,ha),b.aa=b.Y=!1}h=void 0}b.J?f=this.Ha(b.N,h):(this.A(),f=!0);f&&(b.C?this.Ma():this.D(b.N,"beforeChange"),b.N=h,this.D(b.N,"spectate"),!b.C&&a&&this.D(b.N),this.sb&& -this.sb());d&&this.D(b.N,"awake");return f},I:function(a){var b=this[x];(b.Y&&(a||!b.J)||b.C&&this.sa())&&this.U();return b.N},Ja:function(a){var b=this;c.V.fn.Ja.call(b,a);b.pb=()=>{b[x].C||(b[x].aa?b.U():b[x].Y=!1);return b[x].N};b.va=d=>{b.qb(b[x].N);b[x].Y=!0;d&&(b[x].aa=!0);b.rb(b,!d)}},A:function(){var a=this[x];!a.C&&a.B&&c.g.P(a.B,(b,d)=>d.A?.());a.o&&a.$a&&c.g.K.jb(a.o,a.$a);a.B=void 0;a.J=0;a.$=!0;a.aa=!1;a.Y=!1;a.C=!1;a.o=void 0;a.na=void 0;a.Ob=void 0}},la={Ta:function(a){var b=this,d= -b[x];if(!d.$&&d.C&&"change"==a){d.C=!1;if(d.aa||b.sa())d.B=null,d.J=0,b.U()&&b.Ma();else{var f=[];c.g.P(d.B,(h,l)=>f[l.ka]=h);f.forEach((h,l)=>{var e=d.B[h],g=b.Sb(e.X);g.ka=l;g.la=e.la;d.B[h]=g});b.sa()&&b.U()&&b.Ma()}d.$||b.D(d.N,"awake")}},Ra:function(a){var b=this[x];b.$||"change"!=a||this.ra("change")||(c.g.P(b.B,(d,f)=>{f.A&&(b.B[d]={X:f.X,ka:f.ka,la:f.la},f.A())}),b.C=!0,this.D(void 0,"asleep"))},Ca:function(){var a=this[x];a.C&&(a.aa||this.sa())&&this.U();return c.V.fn.Ca.call(this)}};Object.setPrototypeOf(J, -c.V.fn);var S=c.ba.pc;J[S]=c.s;c.v("computed",c.s);c.v("isComputed",a=>"function"==typeof a&&a[S]===J[S]);c.v("computed.fn",J);c.ha(J,"dispose",J.A);c.Nb=a=>{if("function"===typeof a)return c.s(a,{pure:!0});a={...a,pure:!0};return c.s(a)};c.F={R:a=>{switch(a.nodeName){case "OPTION":return!0===a.__ko__hasDomDataOptionValue__?c.g.j.get(a,c.h.options.hb):a.value;case "SELECT":return 0<=a.selectedIndex?c.F.R(a.options[a.selectedIndex]):void 0;default:return a.value}},Oa:(a,b,d)=>{switch(a.nodeName){case "OPTION":"string"=== -typeof b?(c.g.j.set(a,c.h.options.hb,void 0),delete a.__ko__hasDomDataOptionValue__,a.value=b):(c.g.j.set(a,c.h.options.hb,b),a.__ko__hasDomDataOptionValue__=!0,a.value="number"===typeof b?b:"");break;case "SELECT":for(var f=-1,h=""===b||null==b,l=a.options.length,e;l--;)if(e=c.F.R(a.options[l]),e==b||""===e&&h){f=l;break}if(d||0<=f||h&&1{var a=RegExp("\"(?:\\\\.|[^\"])*\"|'(?:\\\\.|[^'])*'|`(?:\\\\.|[^`])*`|/\\*(?:[^*]|\\*+[^*/])*\\*+/|//.*\n|/(?:\\\\.|[^/])+/w*|[^\\s:,/][^,\"'`{}()/:[\\]]*[^\\s,\"'`{}()/:[\\]]|[^\\s]", -"g"),b=/[\])"'A-Za-z0-9_$]+$/,d={"in":1,"return":1,"typeof":1},f=h=>{h=c.g.Rb(h);123===h.charCodeAt(0)&&(h=h.slice(1,-1));h+="\n,";var l=[],e=h.match(a),g=[],k=0;if(1=k){l.push(q&&g.length?{key:q,value:g.join("")}:{unknown:q||g.join("")});var q=k=0;g=[];continue}}else if(58===m){if(!k&&!q&&1===g.length){q=g.pop();continue}}else if(47===m&&1{var l=[],e=(g,k)=>{var n=c.h[g];n?.preprocess&&!n.preprocess(k,g,e)||l.push("'"+g+"':function(){return "+(k+" }"))};f(h).forEach(g=>e(g.key||g.unknown,g.value));return l.join(",")},lc:(h,l)=>-1 -e.key==l),Pa:(h,l,e,g,k)=>{if(h&&c.T(h))!c.Jb(h)||k&&h.I()===g||h(g);else throw Error(`${e} , must be observable`);}}})();(()=>{function a(e){return 8==e.nodeType&&f.test(e.nodeValue)}function b(e){return 8==e.nodeType&&h.test(e.nodeValue)}function d(e,g){for(var k=e,n=1,p=[];k=k.nextSibling;){if(b(k)&&(c.g.j.set(k,l,!0),!--n))return p;p.push(k);a(k)&&++n}if(!g)throw Error("Cannot find closing comment tag to match: "+e.nodeValue);return null}var f=/^\s*ko(?:\s+([\s\S]+))?\s*$/,h=/^\s*\/ko\s*$/,l= -"__ko_matchedEndComment__";c.m={ca:{},childNodes:e=>a(e)?d(e):e.childNodes,oa:e=>{a(e)?(e=d(e))&&[...e].forEach(g=>c.removeNode(g)):c.g.bb(e)},ua:(e,g)=>{a(e)?(c.m.oa(e),e.after(...g)):c.g.ua(e,g)},prepend:(e,g)=>{a(e)?e.nextSibling.before(g):e.prepend(g)},jc:(e,g,k)=>{k?k.after(g):c.m.prepend(e,g)},firstChild:e=>{if(a(e))return e=e.nextSibling,!e||b(e)?null:e;let g=e.firstChild;if(g&&b(g))throw Error("Found invalid end comment, as the first child of "+e);return g},nextSibling:e=>{if(a(e)){var g= -d(e,void 0);e=g?(g.length?g[g.length-1]:e).nextSibling:null}if((g=e.nextSibling)&&b(g)){if(b(g)&&!c.g.j.get(g,l))throw Error("Found end comment without a matching opening comment, as child of "+e);return null}return g},ec:a,tc:e=>(e=e.nodeValue.match(f))?e[1]:null}})();const ba=new Map;c.zb=new class{mc(a){switch(a.nodeType){case 1:return null!=a.getAttribute("data-bind");case 8:return c.m.ec(a)}return!1}cc(a,b){a:{switch(a.nodeType){case 1:var d=a.getAttribute("data-bind");break a;case 8:d=c.m.tc(a); -break a}d=null}if(d)try{let h=ba.get(d);if(!h){var f="with($context){with($data||{}){return{"+c.S.oc(d)+"}}}";h=new Function("$context","$element",f);ba.set(d,h)}return h(b,a)}catch(h){throw h.message="Unable to parse bindings.\nBindings value: "+d+"\nMessage: "+h.message,h;}return null}};const L=Symbol("_subscribable"),N=Symbol("_ancestorBindingInfo"),aa=Symbol("_dataDependency"),ca={},K=c.g.j.Z();c.h={};c.ea=class{constructor(a,b,d,f,h){var l=this,e=a===ca,g=e?void 0:a,k="function"==typeof g&&!c.T(g), -n=h?.dataDependency;a=()=>{var m=k?g():g,q=c.g.i(m);b?(c.g.extend(l,b),N in b&&(l[N]=b[N])):(l.$parents=[],l.$root=q,l.ko=c);l[L]=p;e?q=l.$data:(l.$rawData=m,l.$data=q);d&&(l[d]=q);f?.(l,b,q);if(b?.[L]&&!c.u.s().fb(b[L]))b[L]();n&&(l[aa]=n);return l.$data};if(h?.exportDependencies)a();else{var p=c.Nb(a);p.I();p.isActive()?p.pa=null:l[L]=void 0}}createChildContext(a,b,d,f){!f&&b&&"object"==typeof b&&(f=b,b=f.as,d=f.extend);return new c.ea(a,this,b,(h,l)=>{h.$parentContext=l;h.$parent=l.$data;h.$parents= -(l.$parents||[]).slice(0);h.$parents.unshift(h.$parent);d&&d(h)},f)}extend(a,b){return new c.ea(ca,this,null,d=>c.g.extend(d,"function"==typeof a?a(d):a),b)}};class ma{constructor(a,b,d){this.L=a;this.ia=b;this.xa=new Set;this.G=!1;b.H||c.g.K.ma(a,W);d?.H&&(d.H.xa.add(a),this.Da=d)}Mb(){this.Da?.H?.ac(this.L)}ac(a){this.xa.delete(a);this.xa.size||this.Cb?.()}Cb(){this.G=!0;this.ia.H&&!this.xa.size&&(this.ia.H=null,c.g.K.jb(this.L,W),c.l.notify(this.L,c.l.ga),this.Mb())}}c.l={G:"childrenComplete", -ga:"descendantsComplete",subscribe:(a,b,d,f,h)=>{var l=c.g.j.eb(a,K,{});l.Aa||(l.Aa=new c.V);h?.notifyImmediately&&l.gb[b]&&c.u.M(d,f,[a]);return l.Aa.subscribe(d,f,b)},notify:(a,b)=>{var d=c.g.j.get(a,K);if(d&&(d.gb[b]=!0,d.Aa?.D(a,b),b==c.l.G))if(d.H)d.H.Cb();else if(void 0===d.H&&d.Aa?.ra(c.l.ga))throw Error("descendantsComplete event not supported for bindings on this node");},lb:(a,b)=>{var d=c.g.j.eb(a,K,{});d.H||(d.H=new ma(a,d,b[N]));return b[N]==d?b:b.extend(f=>{f[N]=d})}};c.sc=a=>(a=c.g.j.get(a, -K))&&a.context;c.vb=(a,b,d)=>Z(a,b,Q(d));c.xb=(a,b)=>{1!==b.nodeType&&8!==b.nodeType||X(Q(a),b)};c.wb=function(a,b,d){if(2>arguments.length){if(b=M.body,!b)throw Error("ko.applyBindings: could not find document.body; has the document been loaded?");}else if(!b||1!==b.nodeType&&8!==b.nodeType)throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");Y(Q(a,d),b)};c.Db=a=>(a=a&&[1,8].includes(a.nodeType)&&c.sc(a))?a.$data:void 0;c.v("bindingHandlers", -c.h);c.v("applyBindings",c.wb);c.v("applyBindingAccessorsToNode",c.vb);c.v("dataFor",c.Db);(()=>{var a=Object.create(null),b=new Map;c.Wa={get:(l,e)=>{if(b.has(l))e(b.get(l));else{var g=a[l];g?g.subscribe(e):(g=a[l]=new c.V,g.subscribe(e),h(l,k=>{b.set(l,k);delete a[l];g.D(k)}))}},register:(l,e)=>{if(!e)throw Error("Invalid configuration for "+l);if(d[l])throw Error("Component "+l+" is already registered");d[l]=e}};var d=Object.create(null),f=(l,e)=>{throw Error(`Component '${l}': ${e}`);},h=(l,e)=> -{var g={},k=d[l]||{},n=k.template;k=k.viewModel;if(n){n.element||f(l,"Unknown template value: "+n);n=n.element;var p=M.getElementById(n);p||f(l,"Cannot find element with ID "+n);p.matches("TEMPLATE")||f(l,"Template Source Element not a