diff --git a/.docker/release/files/usr/local/include/application.ini b/.docker/release/files/usr/local/include/application.ini index 4655a1599..f4bb30b77 100644 --- a/.docker/release/files/usr/local/include/application.ini +++ b/.docker/release/files/usr/local/include/application.ini @@ -53,26 +53,44 @@ pdo_password = "" suggestions_limit = 30 [security] -; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery) -csrf_protection = On custom_server_signature = "SnappyMail" x_xss_protection_header = "1; mode=block" openpgp = Off +; Access settings +allow_admin_panel = On + ; Login and password for web admin panel admin_login = "admin" admin_password = "" admin_totp = "" - -; Access settings -allow_admin_panel = On -hide_x_mailer_header = On admin_panel_host = "" admin_panel_key = "admin" +force_https = Off +hide_x_mailer_header = On + +; For example to allow all images use "img-src https:". More info at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives content_security_policy = "" + +; Report CSP errors to PHP and/or SnappyMail Log csp_report = Off + +; A valid cipher method from https://php.net/openssl_get_cipher_methods encrypt_cipher = "aes-256-cbc-hmac-sha1" +; Strict, Lax or None +cookie_samesite = "Strict" + +; Additional allowed Sec-Fetch combinations separated by ";". +; For example: +; * Allow iframe on same domain in any mode: dest=iframe,site=same-origin +; * Allow navigate to iframe on same domain: mode=navigate,dest=iframe,site=same-origin +; * Allow navigate to iframe on (sub)domain: mode=navigate,dest=iframe,site=same-site +; * Allow navigate to iframe from any domain: mode=navigate,dest=iframe,site=cross-site +; +; Default is "site=same-origin;site=none" +secfetch_allow = "" + [admin_panel] allow_update = Off @@ -93,20 +111,35 @@ cafile = "" capath = "" ; Location of client certificate file (pem format with private key) on local filesystem -client_cert = "" +local_cert = "" + +; This can help mitigate the CRIME attack vector. +disable_compression = On [capa] quota = On + +; Allow clear folder and delete messages without moving to trash dangerous_actions = On -message_actions = On + +; Allow download attachments as Zip (and optionally others) attachments_actions = On [login] +; If someone logs in without "@domain.tld", this value will be used +; When this value is HTTP_HOST, the $_SERVER["HTTP_HOST"] value is used. +; When this value is SERVER_NAME, the $_SERVER["SERVER_NAME"] value is used. +; When this value is gethostname, the gethostname() value is used. +; default_domain = "" ; Allow language selection on webmail login screen allow_languages_on_login = On + +; Detect language from browser header `Accept-Language` determine_user_language = On + +; Like default_domain but then HTTP_HOST/SERVER_NAME without www. determine_user_domain = Off login_lowercase = On @@ -123,7 +156,7 @@ sign_me_auto = "DefaultOff" ; Enable plugin support enable = Off -; List of enabled plugins +; Comma-separated list of enabled plugins enabled_list = "" [defaults] @@ -145,6 +178,9 @@ mail_reply_same_folder = Off ; Enable logging enable = Off +; Path where log files will be stored +path = "" + ; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest). ; 0 = Emergency ; 1 = Alert @@ -201,6 +237,8 @@ auth_syslog = On [debug] ; Special option required for development purposes enable = Off +javascript = Off +css = Off [cache] ; The section controls caching of the entire application. @@ -208,6 +246,9 @@ enable = Off ; Enables caching in the system enable = On +; Path where cache files will be stored +path = "" + ; Additional caching key. If changed, cache is purged index = "v1" @@ -225,55 +266,41 @@ http_expires = 3600 ; Caching message UIDs when searching and sorting (threading) server_uids = On +system_data = On [imap] -use_sort = On use_force_selection = Off -use_thread = On -use_move = Off use_expunge_all_on_delete = Off -body_text_limit = 555000 message_list_fast_simple_search = On -message_list_count_limit_trigger = 0 -message_list_date_filter = 0 message_list_permanent_filter = "" message_all_headers = Off -large_thread_limit = 50 -folder_list_limit = 200 show_login_alert = On -use_list_status = On -timeout = 300 -disable_metadata = Off +fetch_new_messages = On [labs] -cache_system_data = On +; Display message RFC 2822 date and time header, instead of the arrival internal date. date_from_headers = On -autocreate_system_folders = Off allow_message_append = Off -login_fault_delay = 1 + +; When login fails, wait N seconds before responding +login_fault_delay = 5 log_ajax_response_write_limit = 300 allow_html_editor_biti_buttons = Off allow_ctrl_enter_on_compose = On -try_to_detect_hidden_images = Off -use_app_debug_js = Off -use_app_debug_css = Off smtp_show_server_errors = Off -smtp_timeout = 60 sieve_auth_plain_initial = On sieve_allow_fileinto_inbox = Off -sieve_timeout = 10 -sasl_allow_plain = On -sasl_allow_scram_sha = Off -sasl_allow_cram_md5 = Off + +; PHP mail() remove To and Subject headers mail_func_clear_headers = On + +; PHP mail() set -f emailaddress mail_func_additional_parameters = Off folders_spec_limit = 50 curl_proxy = "" curl_proxy_auth = "" -force_https = Off custom_login_link = "" custom_logout_link = "" -allow_external_login = Off http_client_ip_check_proxy = Off fast_cache_memcache_host = "127.0.0.1" fast_cache_memcache_port = 11211 @@ -283,12 +310,11 @@ use_local_proxy_for_external_images = On image_exif_auto_rotate = Off cookie_default_path = "" cookie_default_secure = Off -check_new_messages = On replace_env_in_configuration = "" boundary_prefix = "" dev_email = "" dev_password = "" [version] -current = "2.20.0" -saved = "Fri, 30 Sep 2022 12:34:56 +0000" +current = "2.28.4" +saved = "Sun, 18 Dec 2022 22:10:48 +0000" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2f48abb98..d53d388ed 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,6 +28,7 @@ If applicable, add screenshots to help explain your problem. - IMAP daemon: [e.g. courier, dovecot] - PHP version: - SnappyMail Version: +- Mode: [e.g. standalone, nextcloud, cyberpanel, docker] **[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)** Place them here (few lines) or as attachments (many lines) diff --git a/.gitignore b/.gitignore index 46428042c..b4e8f72e4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /data /MULTIPLY /include.php +/issues .idea/ .env /test diff --git a/.htaccess b/.htaccess index 449dcc730..faa8201c8 100644 --- a/.htaccess +++ b/.htaccess @@ -26,7 +26,6 @@ # Header set Strict-Transport-Security "max-age=31536000" Header set imagetoolbar "no" # Header set X-Content-Type-Options "nosniff" -# Header set X-Frame-Options "DENY" # Header set X-XSS-Protection "1; mode=block" Header set Service-Worker-Allowed "/" diff --git a/CHANGELOG.md b/CHANGELOG.md index 642c3421f..c655d05bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1447 @@ +## 2.28.4 – 2023-07-10 + +## Added +- application.ini msg_default_action by @SergeyMosin + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) +- application.ini view_show_next_message by @SergeyMosin + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) +- application.ini view_images by @SergeyMosin + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) +- nextcloud add ability to include custom php file in InstallStep migration by @SergeyMosin + [#1197](https://github.com/the-djmaze/snappymail/pull/1197) +- Support plugin for Squire editor + [#1192](https://github.com/the-djmaze/snappymail/pull/1192) + +## Changed +- only show 'Add "domain.tld" as an application for mailto links?' message after login (firefox shows the message on every reload otherwise). + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) +- Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance +- New bidi buttons to Squire editor by @rezaei92 + [#1200](https://github.com/the-djmaze/snappymail/pull/1200) + +## Fixed +- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT + [#1205](https://github.com/the-djmaze/snappymail/pull/1205) +- 'reloadTime' function result is passed into 'setInterval' instead of the function by @SergeyMosin + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) +- UNKNOWN-CTE Invalid data in MIME part + [#1186](https://github.com/the-djmaze/snappymail/pull/1186) + + +## 2.28.3 – 2023-06-22 + +## Added +- Attachments in "new window" view + [#1166](https://github.com/the-djmaze/snappymail/pull/1166) + +## Changed +- Update Portuguese by @ner00 +- Update French by @hguilbert + +## Fixed +- Some emails with inline CSS break the UI + [#1187](https://github.com/the-djmaze/snappymail/pull/1187) +- Remote.get() Promise broken by previous change + [#1185](https://github.com/the-djmaze/snappymail/pull/1185) +- Class "MailSo\Base\Exceptions\InvalidArgumentException" not found + [#1182](https://github.com/the-djmaze/snappymail/pull/1182) +- First account not showed in the right list (dropbox) + [#1180](https://github.com/the-djmaze/snappymail/pull/1180) + + +## 2.28.2 – 2023-06-19 + +## Added +- Detailed error for "Cannot access the repository at the moment" + [#1164](https://github.com/the-djmaze/snappymail/pull/1164) +- Bidi in Squire editor + [#1158](https://github.com/the-djmaze/snappymail/pull/1158) +- Translate Squire UI +- Nextcloud 27 compatibility by @LarsBel +- JWT class for handling JSON Web Tokens + +## Changed +- Update German by @cm-schl +- Update French by @hguilbert +- Update Polish by @tinola +- Merge handling of local Account Settings. Found while investigating + [#1170](https://github.com/the-djmaze/snappymail/pull/1170) +- Image max-width now 100% instead of 90vw + +## Fixed +- Cannot modify header information + [#929](https://github.com/the-djmaze/snappymail/pull/929) (comment) +- Admin Panel broken when admin_panel_host is set + [#1169](https://github.com/the-djmaze/snappymail/pull/1169) +- Invalid CSP report-uri +- Prevent MessageList multiple request at the same time + [#1071](https://github.com/the-djmaze/snappymail/pull/1071) +- Error in Addressbook Sync + [#1179](https://github.com/the-djmaze/snappymail/pull/1179) +- base64_decode() second parameter must be true + + +## 2.28.1 – 2023-06-05 + +## Changed +- Optical issue with input fields for mail and folder search + [#1149](https://github.com/the-djmaze/snappymail/pull/1149) +- Update Chinese translation by @mayswind + [#1157](https://github.com/the-djmaze/snappymail/pull/1157) +- Update Polish translation by @tinola + [#1156](https://github.com/the-djmaze/snappymail/pull/1156) + +## Fixed +- Undefined SIG constants + [#1147](https://github.com/the-djmaze/snappymail/pull/1147) + + +## 2.28.0 – 2023-05-30 + +## Added +- Threaded view make number orange when unread sub-messages + [#1028](https://github.com/the-djmaze/snappymail/pull/1028) +- Handle PHP pctnl messages +- addEventListener('rl-view-model') missing for Settings + [#1013](https://github.com/the-djmaze/snappymail/pull/1013) +- CSS `--btn-border-radius` + +## Changed +- Improved RTL languages support + [#1056](https://github.com/the-djmaze/snappymail/pull/1056) +- Composer text/attachments as tabs + [#1119](https://github.com/the-djmaze/snappymail/pull/1119) +- Filter dialog doesn't refer to folder names consistently + [#1111](https://github.com/the-djmaze/snappymail/pull/1111) +- TLS connection for MYSQL contact db + [#1078](https://github.com/the-djmaze/snappymail/pull/1078) +- Allow empty message body when there are attachments + [#1052](https://github.com/the-djmaze/snappymail/pull/1052) +- PHP inherit logger as Trait +- Update Portuguese by @ner00 + [#1124](https://github.com/the-djmaze/snappymail/pull/1124) +- Update Traditional Chinese (Taiwan) by @chiyi4488 + [#1107](https://github.com/the-djmaze/snappymail/pull/1107) +- Update Russian by @konkere + [#1108](https://github.com/the-djmaze/snappymail/pull/1108) +- Update Italian by @cm-schl + [#1094](https://github.com/the-djmaze/snappymail/pull/1094) +- Update French by @hguilbert + [#1102](https://github.com/the-djmaze/snappymail/pull/1102) +- Update German by @cm-schl + [#1087](https://github.com/the-djmaze/snappymail/pull/1087) + +## Fixed +- Show messagelist timeout/abort error for + [#1071](https://github.com/the-djmaze/snappymail/pull/1071) +- DesktopNotifications setting not saved + [#1137](https://github.com/the-djmaze/snappymail/pull/1137) +- PHP Deprecation warning for $_openPipes + [#1141](https://github.com/the-djmaze/snappymail/pull/1141) +- Images size wrong + [#1134](https://github.com/the-djmaze/snappymail/pull/1134) +- Unable to preview body of encrypted mail in mailvelope reply-to + [#1130](https://github.com/the-djmaze/snappymail/pull/1130) +- Replace ` + + + diff --git a/plugins/override-smtp-credentials/index.php b/plugins/override-smtp-credentials/index.php index 893d7a513..063d10508 100644 --- a/plugins/override-smtp-credentials/index.php +++ b/plugins/override-smtp-credentials/index.php @@ -4,61 +4,71 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Override SMTP Credentials', - VERSION = '2.2', - RELEASE = '2022-04-13', - REQUIRED = '2.5.0', + VERSION = '2.4', + RELEASE = '2023-01-19', + REQUIRED = '2.23.0', CATEGORY = 'Filters', DESCRIPTION = 'Override SMTP credentials for specific users.'; public function Init() : void { - $this->addHook('smtp.before-connect', 'FilterSmtpCredentials'); + $this->addHook('smtp.before-connect', 'FilterSmtpConnect'); $this->addHook('smtp.before-login', 'FilterSmtpCredentials'); } /** * @param \RainLoop\Model\Account $oAccount * @param \MailSo\Smtp\SmtpClient $oSmtpClient - * @param array $aSmtpCredentials + * @param \MailSo\Smtp\Settings $oSettings */ - public function FilterSmtpCredentials($oAccount, $oSmtpClient, &$aSmtpCredentials) + public function FilterSmtpConnect(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) { - if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials)) - { - $sEmail = $oAccount->Email(); - + $sEmail = $oAccount->Email(); + $sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', '')); + $sFoundValue = ''; + if (\strlen($sWhiteList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList, $sFoundValue)) { + \SnappyMail\LOG::debug('SMTP Override', "{$sEmail} matched {$sFoundValue}"); + $oSettings->usePhpMail = false; $sHost = \trim($this->Config()->Get('plugin', 'smtp_host', '')); - $sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', '')); - - $sFoundValue = ''; - if (0 < strlen($sWhiteList) && 0 < \strlen($sHost) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList, $sFoundValue)) - { - \SnappyMail\LOG::debug('SMTP Override', "{$sEmail} matched {$sFoundValue}"); - $aSmtpCredentials['Host'] = $sHost; - $aSmtpCredentials['Port'] = (int) $this->Config()->Get('plugin', 'smtp_port', 25); - + if (\strlen($sHost)) { + $oSettings->host = $sHost; + $oSettings->port = (int) $this->Config()->Get('plugin', 'smtp_port', 25); $sSecure = \trim($this->Config()->Get('plugin', 'smtp_secure', 'None')); switch ($sSecure) { case 'SSL': - $aSmtpCredentials['Secure'] = MailSo\Net\Enumerations\ConnectionSecurityType::SSL; + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::SSL; break; case 'TLS': - $aSmtpCredentials['Secure'] = MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS; + case 'STARTTLS': + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS; + break; + case 'Detect': + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT; break; default: - $aSmtpCredentials['Secure'] = MailSo\Net\Enumerations\ConnectionSecurityType::NONE; + $oSettings->type = MailSo\Net\Enumerations\ConnectionSecurityType::NONE; break; } + } + } else { + \SnappyMail\LOG::debug('SMTP Override', "{$sEmail} no match"); + } + } - $aSmtpCredentials['UseAuth'] = (bool) $this->Config()->Get('plugin', 'smtp_auth', true); - $aSmtpCredentials['Login'] = \trim($this->Config()->Get('plugin', 'smtp_user', '')); - $aSmtpCredentials['Password'] = (string) $this->Config()->Get('plugin', 'smtp_password', ''); - } - else - { - \SnappyMail\LOG::debug('SMTP Override', "{$sEmail} no match"); - } + /** + * @param \RainLoop\Model\Account $oAccount + * @param \MailSo\Smtp\SmtpClient $oSmtpClient + * @param \MailSo\Smtp\Settings $oSettings + */ + public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) + { + $sWhiteList = \trim($this->Config()->Get('plugin', 'override_users', '')); + $sFoundValue = ''; + if (\strlen($sWhiteList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $sWhiteList, $sFoundValue)) { + $oSettings->useAuth = (bool) $this->Config()->Get('plugin', 'smtp_auth', true); + $oSettings->Login = \trim($this->Config()->Get('plugin', 'smtp_user', '')); + $oSettings->Password = (string) $this->Config()->Get('plugin', 'smtp_password', ''); } } @@ -75,7 +85,7 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin ->SetDefaultValue(25), \RainLoop\Plugins\Property::NewInstance('smtp_secure')->SetLabel('SMTP Secure') ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) - ->SetDefaultValue(array('None', 'SSL', 'TLS')), + ->SetDefaultValue(array('None', 'Detect', 'SSL', 'STARTTLS')), \RainLoop\Plugins\Property::NewInstance('smtp_auth')->SetLabel('Use auth') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDefaultValue(true), diff --git a/plugins/proxyauth-login-example/index.php b/plugins/proxyauth-login-example/index.php index 675e74900..bdab43fe9 100644 --- a/plugins/proxyauth-login-example/index.php +++ b/plugins/proxyauth-login-example/index.php @@ -4,8 +4,9 @@ class ProxyauthLoginExamplePlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Proxy Auth Login Example', - VERSION = '2.1', - REQUIRED = '2.5.0', + VERSION = '2.2', + RELEASE = '2022-12-08', + REQUIRED = '2.23.0', CATEGORY = 'General', DESCRIPTION = ''; @@ -31,12 +32,12 @@ class ProxyauthLoginExamplePlugin extends \RainLoop\Plugins\AbstractPlugin if ($oAccount instanceof \RainLoop\Model\Account) { // Verify logic - $bValid = $this->isValidAccount($oAccount->Login(), $oAccount->Password()); + $bValid = $this->isValidAccount($oAccount->IncLogin(), $oAccount->IncPassword()); /** * $oAccount->Email(); // Email (It is not a IMAP login) - * $oAccount->Login(); // IMAP login - * $oAccount->Password(); // IMAP password + * $oAccount->IncLogin(); // IMAP login + * $oAccount->IncPassword(); // IMAP password * $oAccount->Domain()->IncHost(); // IMAP host * * @see \RainLoo\Model\Account for more diff --git a/plugins/recaptcha/index.php b/plugins/recaptcha/index.php index a0fd55ffe..9d76e4a7d 100644 --- a/plugins/recaptcha/index.php +++ b/plugins/recaptcha/index.php @@ -6,9 +6,9 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'reCaptcha', AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', - VERSION = '2.12.1', - RELEASE = '2022-02-14', - REQUIRED = '2.12.1', + VERSION = '2.15', + RELEASE = '2023-02-22', + REQUIRED = '2.26.4', CATEGORY = 'General', LICENSE = 'MIT', DESCRIPTION = 'A CAPTCHA (v2) is a program that can generate and grade tests that humans can pass but current computer programs cannot. For example, humans can read distorted text as the one shown below, but current computer programs can\'t. More info at https://developers.google.com/recaptcha'; @@ -22,8 +22,8 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs('js/recaptcha.js'); - $this->addHook('json.action-pre-call', 'AjaxActionPreCall'); - $this->addHook('filter.json-response', 'FilterAjaxResponse'); + $this->addHook('json.before-login', 'BeforeLogin'); + $this->addHook('json.after-login', 'AfterLogin'); $this->addHook('main.content-security-policy', 'ContentSecurityPolicy'); } @@ -82,16 +82,13 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin } } - /** - * @param string $sAction - */ - public function AjaxActionPreCall(string $sAction) + public function BeforeLogin() { - if ('Login' === $sAction && 0 >= $this->getLimit()) { + if (0 >= $this->getLimit()) { $bResult = false; $HTTP = \SnappyMail\HTTP\Request::factory(); - $oResponse = $HTTP->doRequest('POST', 'https://www.google.com/recaptcha/api/siteverify', array( + $oResponse = $HTTP->doRequest('POST', 'https://www.recaptcha.net/recaptcha/api/siteverify', array( 'secret' => $this->Config()->Get('plugin', 'private_key', ''), 'response' => $this->Manager()->Actions()->GetActionParam('RecaptchaResponse', '') )); @@ -112,18 +109,18 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin /** * @param string $sAction - * @param array $aResponseItem + * @param array $aResponse */ - public function FilterAjaxResponse(string $sAction, array &$aResponseItem) + public function AfterLogin(array &$aResponse) { - if ('Login' === $sAction && $aResponseItem && isset($aResponseItem['Result'])) { + if (isset($aResponse['Result'])) { $oCacher = $this->Manager()->Actions()->Cacher(); $iConfigLimit = (int) $this->Config()->Get('plugin', 'error_limit', 0); $sKey = $this->getCaptchaCacherKey(); if (0 < $iConfigLimit && $oCacher && $oCacher->IsInited()) { - if (false === $aResponseItem['Result']) { + if (false === $aResponse['Result']) { $iLimit = 0; $sLimut = $oCacher->Get($sKey); if (\strlen($sLimut) && \is_numeric($sLimut)) { @@ -133,7 +130,7 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin $oCacher->Set($sKey, ++$iLimit); if ($iConfigLimit <= $iLimit) { - $aResponseItem['Captcha'] = true; + $aResponse['Captcha'] = true; } } else { $oCacher->Delete($sKey); @@ -144,10 +141,9 @@ class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP) { - $CSP->script[] = 'https://www.google.com/recaptcha/'; - $CSP->script[] = 'https://www.gstatic.com/recaptcha/'; - $CSP->frame[] = 'https://www.google.com/recaptcha/'; - $CSP->frame[] = 'https://recaptcha.google.com/recaptcha/'; + $CSP->add('script-src', 'https://www.gstatic.com/recaptcha/'); + $CSP->add('script-src', 'https://www.recaptcha.net/recaptcha/'); + $CSP->add('frame-src', 'https://www.recaptcha.net/recaptcha/'); } } diff --git a/plugins/recaptcha/js/recaptcha.js b/plugins/recaptcha/js/recaptcha.js index 246d7adc2..591d26598 100644 --- a/plugins/recaptcha/js/recaptcha.js +++ b/plugins/recaptcha/js/recaptcha.js @@ -36,7 +36,7 @@ } else if (!script) { script = doc.createElement('script'); // script.onload = ShowRecaptcha; - script.src = 'https://www.google.com/recaptcha/api.js?onload=ShowRecaptcha&render=explicit&hl=' + doc.documentElement.lang; + script.src = 'https://www.recaptcha.net/recaptcha/api.js?onload=ShowRecaptcha&render=explicit&hl=' + doc.documentElement.lang; doc.head.append(script); } }; diff --git a/plugins/set-remote-addr/LICENSE b/plugins/set-remote-addr/LICENSE new file mode 100644 index 000000000..271342337 --- /dev/null +++ b/plugins/set-remote-addr/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 RainLoop Team + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/set-remote-addr/index.php b/plugins/set-remote-addr/index.php new file mode 100644 index 000000000..dbc10f8a6 --- /dev/null +++ b/plugins/set-remote-addr/index.php @@ -0,0 +1,23 @@ +jsonResponse(__FUNCTION__, false); } - $oSettings = $this->Manager()->Actions()->SettingsProvider()->Load($oAccount); - if ($this->Manager()->Actions()->HasActionParam('EnableTwoFactor')) { - $sValue = $this->GetActionParam('EnableTwoFactor', ''); - $oSettings->SetConf('EnableTwoFactor', !empty($sValue)); + $oActions = $this->Manager()->Actions(); + if ($oActions->HasActionParam('EnableTwoFactor')) { + $sValue = $oActions->GetActionParam('EnableTwoFactor', ''); + $oActions->SettingsProvider()->Load($oAccount)->SetConf('EnableTwoFactor', !empty($sValue)); } $sEmail = $oAccount->Email(); diff --git a/plugins/two-factor-auth/langs/pl_PL.ini b/plugins/two-factor-auth/langs/pl_PL.ini new file mode 100644 index 000000000..2e00f7683 --- /dev/null +++ b/plugins/two-factor-auth/langs/pl_PL.ini @@ -0,0 +1,20 @@ +[PLUGIN_2FA] +LEGEND_TWO_FACTOR_AUTH = "2FA (TOTP)" +LABEL_ENABLE_TWO_FACTOR = "Włącz weryfikację 2-stopniową" +LABEL_TWO_FACTOR_USER = "Użytkownik" +LABEL_TWO_FACTOR_STATUS = "Status" +LABEL_TWO_FACTOR_SECRET = "Twój sekretny klucz" +LABEL_TWO_FACTOR_BACKUP_CODES = "Kody zapasowe" +BUTTON_CREATE = "Utwórz sekretny klucz" +BUTTON_ACTIVATE = "Aktywuj" +LINK_TEST = "Testuj" +BUTTON_SHOW_SECRET = "Pokaż Twój sekretny klucz" +BUTTON_HIDE_SECRET = "Ukryj klucz" +TWO_FACTOR_SECRET_CONFIGURED_DESC = "Skonfigurowano" +TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nieskonfigurowano" +TWO_FACTOR_SECRET_DESC = "Zaimportuj tę infomację do aplikacji Google Authenticator (lub innego klienta TOTP) używając kodu QR poniżej lub wpisz sekretny klucz ręcznie." +TWO_FACTOR_BACKUP_CODES_DESC = "Jeśli nie możesz uzyskać kodów poprzez aplikację Google Authenticator (lub innego klienta TOTP), możesz użyć kodów zapasowych aby się zalogować. Po użyciu kodu zapasowego do zalogowania, stanie się on nieaktywny." +TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Nie możesz zmienić tego ustawienia przed testem." +TITLE_TEST_CODE = "Test weryfikacji 2-stopniowej" +LABEL_CODE = "Kod" +LABEL_TWO_FACTOR_CODE = "Kod weryfikacyjny" diff --git a/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html b/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html index df5a40476..95569afd0 100644 --- a/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html +++ b/plugins/two-factor-auth/templates/TwoFactorAuthSettings.html @@ -47,7 +47,7 @@

-

+				

 			
 		
 		
diff --git a/release.php b/release.php index a784a62ea..4bef9861f 100755 --- a/release.php +++ b/release.php @@ -1,273 +1,3 @@ #!/usr/bin/php version}", file_get_contents($file))); -$file = __DIR__ . '/integrations/cloudron/DESCRIPTION.md'; -file_put_contents($file, preg_replace('/[^<]*{$package->version}<", file_get_contents($file))); -// docker -$file = __DIR__ . '/.docker/release/files/usr/local/include/application.ini'; -file_put_contents($file, preg_replace('/current = "[0-9.]+"/', "current = \"{$package->version}\"", file_get_contents($file))); -// nextcloud -$file = __DIR__ . '/integrations/nextcloud/snappymail/appinfo/info.xml'; -file_put_contents($file, preg_replace('/[^<]*{$package->version}<", file_get_contents($file))); -// virtualmin -$file = __DIR__ . '/integrations/virtualmin/snappymail.pl'; -file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file))); - -if (isset($options['set-version'])) { - exit; -} - -// Arch User Repository -// https://aur.archlinux.org/packages/snappymail/ -$options['aur'] = isset($options['aur']); - -// Docker build -$docker = trim(`which docker`); -$options['docker'] = isset($options['docker']) || (!$options['aur'] && $docker && strtoupper(readline("Build Docker image? (Y/N): ")) === "Y"); - -$destPath = "build/dist/releases/webmail/{$package->version}/"; -is_dir($destPath) || mkdir($destPath, 0777, true); - -$zip_destination = "{$destPath}snappymail-{$package->version}.zip"; -$tar_destination = "{$destPath}snappymail-{$package->version}.tar"; - -@unlink($zip_destination); -@unlink($tar_destination); -@unlink("{$tar_destination}.gz"); - -if (!isset($options['skip-gulp'])) { - echo "\x1b[33;1m === Gulp === \x1b[0m\n"; - passthru($gulp, $return_var); - if ($return_var) { - exit("gulp failed with error code {$return_var}\n"); - } - - $cmddir = escapeshellcmd(__DIR__) . '/snappymail/v/0.0.0/static'; - - if ($gzip = trim(`which gzip`)) { - echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n"; - passthru("{$gzip} -k --best {$cmddir}/js/*.js"); - passthru("{$gzip} -k --best {$cmddir}/js/min/*.js"); - passthru("{$gzip} -k --best {$cmddir}/css/admin*.css"); - passthru("{$gzip} -k --best {$cmddir}/css/app*.css"); - unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/boot.js.gz'); - unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz'); - } - - if ($brotli = trim(`which brotli`)) { - echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n"; - passthru("{$brotli} -k --best {$cmddir}/js/*.js"); - passthru("{$brotli} -k --best {$cmddir}/js/min/*.js"); - passthru("{$brotli} -k --best {$cmddir}/css/admin*.css"); - passthru("{$brotli} -k --best {$cmddir}/css/app*.css"); - unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/boot.js.br'); - unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br'); - } -} - -// Temporary rename folder to speed up PharData -//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){ -if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) { - exit('Failed to temporary rename snappymail/v/0.0.0'); -} -register_shutdown_function(function(){ - // Rename folder back to original - @rename("snappymail/v/{$GLOBALS['package']->version}", 'snappymail/v/0.0.0'); -}); - -echo "\x1b[33;1m === Zip/Tar === \x1b[0m\n"; - -$zip = new ZipArchive(); -if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) { - exit("Failed to create {$zip_destination}"); -} - -$tar = new PharData($tar_destination); - -$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST); -foreach ($files as $file) { - $file = str_replace('\\', '/', $file); - //echo "{$file}\n"; - // Ignore "." and ".." folders - if (!in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) { - if (is_dir($file)) { - $zip->addEmptyDir($file); - } else if (is_file($file)) { - $zip->addFile($file); - } - } -} - -if ($options['docker']) { - $tar->buildFromDirectory('./snappymail/', "@v/{$package->version}@"); -} else { - $tar->buildFromDirectory('./', "@snappymail/v/{$package->version}@"); -} - -//$zip->addFile('cli/upgrade.sh'); -//$tar->addFile('cli/upgrade.sh'); - -$zip->addFile('data/.htaccess'); -$tar->addFile('data/.htaccess'); - -$zip->addFromString('data/VERSION', $package->version); -$tar->addFromString('data/VERSION', $package->version); - -$zip->addFile('data/README.md'); -$tar->addFile('data/README.md'); - -if ($options['aur']) { - $data = 'addFromString('include.php', $data); - $tar->addFromString('include.php', $data); -} else { - $zip->addFile('_include.php'); - $tar->addFile('_include.php'); -} - -$zip->addFile('.htaccess'); -$tar->addFile('.htaccess'); - -$index = file_get_contents('index.php'); -$index = str_replace('0.0.0', $package->version, $index); -$zip->addFromString('index.php', $index); -$tar->addFromString('index.php', $index); - -$zip->addFile('README.md'); -$tar->addFile('README.md'); - -$data = file_get_contents('dev/serviceworker.js'); -//$data = file_get_contents('snappymail/v/0.0.0/static/js/min/serviceworker.min.js'); -$zip->addFromString('serviceworker.js', $data); -$tar->addFromString('serviceworker.js', $data); - -$zip->close(); - -$tar->compress(Phar::GZ); -unlink($tar_destination); -$tar_destination .= '.gz'; - -echo "{$zip_destination} created\n{$tar_destination} created\n"; - -if (isset($options['nextcloud'])) { - require(__DIR__ . '/build/nextcloud.php'); -} - -rename("snappymail/v/{$package->version}", 'snappymail/v/0.0.0'); - -file_put_contents("{$destPath}core.json", '{ - "version": "'.$package->version.'", - "file": "../latest.tar.gz", - "warnings": [] -}'); - -// Arch User Repository -if ($options['aur']) { - // extension_loaded('blake2') - if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) { - function b2sum($file) { - $file = escapeshellarg($file); - exec("b2sum --binary {$file} 2>&1", $output, $exitcode); - $output = explode(' ', implode("\n", $output)); - return $output[0]; - } - } - - $b2sums = function_exists('b2sum') ? [ - b2sum($tar_destination), - b2sum(__DIR__ . '/build/arch/snappymail.sysusers'), - b2sum(__DIR__ . '/build/arch/snappymail.tmpfiles') - ] : []; - - file_put_contents('build/arch/.SRCINFO', 'pkgbase = snappymail - pkgdesc = modern PHP webmail client - pkgver = '.$package->version.' - pkgrel = 1 - url = https://github.com/the-djmaze/snappymail - arch = any - license = AGPL3 - makedepends = php - makedepends = nodejs - makedepends = yarn - makedepends = gulp - depends = php-fpm - optdepends = mariadb: storage backend for contacts - optdepends = php-pgsql: storage backend for contacts - optdepends = php-sqlite: storage backend for contacts - source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz - source = snappymail.sysusers - source = snappymail.tmpfiles - b2sums = '.implode("\n b2sums = ", $b2sums).' - -pkgname = snappymail -'); - - $file = __DIR__ . '/build/arch/PKGBUILD'; - if (is_file($file)) { - $PKGBUILD = file_get_contents($file); - $PKGBUILD = preg_replace('/pkgver=[0-9.]+/', "pkgver={$package->version}", $PKGBUILD); - $PKGBUILD = preg_replace('/b2sums=\\([^)]+\\)/s', "b2sums=('".implode("'\n '", $b2sums)."')", $PKGBUILD); - file_put_contents($file, $PKGBUILD); - } -} -// Debian Repository -else if (isset($options['debian'])) { - require(__DIR__ . '/build/deb.php'); -} -// Docker build -else if ($options['docker']) { - echo "\x1b[33;1m === Docker === \x1b[0m\n"; - $zip_filename = "snappymail-{$package->version}.zip"; - copy($zip_destination, "./.docker/release/{$zip_filename}"); - if ($docker) { - passthru("{$docker} build --pull " . __DIR__ . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}"); - } else { - echo "Docker not installed!\n"; - } -} - -if (isset($options['sign'])) { - echo "\x1b[33;1m === PGP Sign === \x1b[0m\n"; - passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tar_destination), $return_var); - passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($zip_destination), $return_var); - if (isset($options['nextcloud'])) { - passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign ' - .escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var); - } - if (isset($options['debian'])) { - passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign ' - . escapeshellarg(__DIR__ . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var); - // https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588 - passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 --clearsign --output ' - . escapeshellarg(__DIR__ . "/build/dist/releases/webmail/{$package->version}/InRelease") . ' ' - . escapeshellarg(__DIR__ . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var); - passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 -abs --output ' - . escapeshellarg(__DIR__ . "/build/dist/releases/webmail/{$package->version}/Release.gpg") . ' ' - . escapeshellarg(__DIR__ . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var); - } -} +require __DIR__ . '/cli/release.php'; diff --git a/snappymail/v/0.0.0/app/domains/default.json b/snappymail/v/0.0.0/app/domains/default.json index 14ab64608..b2d2358bd 100644 --- a/snappymail/v/0.0.0/app/domains/default.json +++ b/snappymail/v/0.0.0/app/domains/default.json @@ -1,18 +1,89 @@ { - "imapHost": "localhost", - "imapPort": 143, - "imapSecure": 0, - "imapShortLogin": false, - "useSieve": false, - "sieveHost": "localhost", - "sievePort": 4190, - "sieveSecure": 0, - "smtpHost": "localhost", - "smtpPort": 25, - "smtpSecure": 0, - "smtpShortLogin": false, - "smtpAuth": true, - "smtpSetSender": false, - "smtpPhpMail": false, + "name": "*", + "IMAP": { + "host": "localhost", + "port": 143, + "type": 0, + "timeout": 300, + "shortLogin": false, + "sasl": [ + "SCRAM-SHA3-512", + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + "PLAIN", + "LOGIN" + ], + "ssl": { + "verify_peer": false, + "verify_peer_name": false, + "allow_self_signed": false, + "SNI_enabled": true, + "disable_compression": true, + "security_level": 1 + }, + "disable_list_status": false, + "disable_metadata": false, + "disable_move": false, + "disable_sort": false, + "disable_thread": false, + "disable_binary": false, + "use_expunge_all_on_delete": false, + "fast_simple_search": true, + "force_select": false, + "message_all_headers": false, + "message_list_limit": 0, + "search_filter": "" + }, + "SMTP": { + "host": "localhost", + "port": 25, + "type": 0, + "timeout": 60, + "shortLogin": false, + "sasl": [ + "SCRAM-SHA3-512", + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + "PLAIN", + "LOGIN" + ], + "ssl": { + "verify_peer": false, + "verify_peer_name": false, + "allow_self_signed": false, + "SNI_enabled": true, + "disable_compression": true, + "security_level": 1 + }, + "useAuth": false, + "setSender": false, + "usePhpMail": false + }, + "Sieve": { + "host": "localhost", + "port": 4190, + "type": 0, + "timeout": 10, + "shortLogin": false, + "sasl": [ + "SCRAM-SHA3-512", + "SCRAM-SHA-512", + "SCRAM-SHA-256", + "SCRAM-SHA-1", + "PLAIN", + "LOGIN" + ], + "ssl": { + "verify_peer": false, + "verify_peer_name": false, + "allow_self_signed": false, + "SNI_enabled": true, + "disable_compression": true, + "security_level": 1 + }, + "enabled": false + }, "whiteList": "" } diff --git a/snappymail/v/0.0.0/app/domains/disabled b/snappymail/v/0.0.0/app/domains/disabled index 0370511f7..09fd760ff 100644 --- a/snappymail/v/0.0.0/app/domains/disabled +++ b/snappymail/v/0.0.0/app/domains/disabled @@ -1 +1 @@ -outlook.com,qq.com,yahoo.com \ No newline at end of file +outlook.com,qq.com,yahoo.com,gmail.com,hotmail.com \ No newline at end of file diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php index 0605b89cc..634c5a3c7 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Collection.php @@ -39,6 +39,11 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable } } + public function keys() : array + { + return \array_keys($this->getArrayCopy()); + } + public function Add($mItem, bool $bToTop = false) : self { $this->append($mItem, $bToTop); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php index 6d642a188..e90af817e 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/DateTimeHelper.php @@ -23,8 +23,7 @@ abstract class DateTimeHelper public static function GetUtcTimeZoneObject() : \DateTimeZone { static $oDateTimeZone = null; - if (null === $oDateTimeZone) - { + if (null === $oDateTimeZone) { $oDateTimeZone = new \DateTimeZone('UTC'); } return $oDateTimeZone; @@ -37,8 +36,7 @@ abstract class DateTimeHelper public static function ParseRFC2822DateString(string $sDateTime) : int { $sDateTime = \trim($sDateTime); - if (empty($sDateTime)) - { + if (empty($sDateTime)) { return 0; } @@ -54,13 +52,12 @@ abstract class DateTimeHelper public static function ParseInternalDateString(string $sDateTime) : int { $sDateTime = \trim($sDateTime); - if (empty($sDateTime)) - { + if (empty($sDateTime)) { return 0; } - if (\preg_match('/^[a-z]{2,4}, /i', $sDateTime)) // RFC2822 ~ "Thu, 10 Jun 2010 08:58:33 -0700 (PDT)" - { + // RFC2822 ~ "Thu, 10 Jun 2010 08:58:33 -0700 (PDT)" + if (\preg_match('/^[a-z]{2,4}, /i', $sDateTime)) { return static::ParseRFC2822DateString($sDateTime); } @@ -74,8 +71,7 @@ abstract class DateTimeHelper public static function ParseDateStringType1(string $sDateTime) : int { $sDateTime = \trim($sDateTime); - if (empty($sDateTime)) - { + if (empty($sDateTime)) { return 0; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php index 106ca4069..794f4c760 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php @@ -23,8 +23,7 @@ abstract class HtmlUtils public static function BuildHtml(string $sHtml, array &$aFoundCids, array &$aFoundDataURL, array &$aFoundContentLocationUrls) : string { $bState = true; - if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors')) - { + if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors')) { $bState = \libxml_use_internal_errors(true); } @@ -86,13 +85,11 @@ abstract class HtmlUtils $oDoc->normalizeDocument(); - if (\MailSo\Base\Utils::FunctionCallable('libxml_clear_errors')) - { + if (\MailSo\Base\Utils::FunctionCallable('libxml_clear_errors')) { \libxml_clear_errors(); } - if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors')) - { + if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors')) { \libxml_use_internal_errors($bState); } @@ -100,63 +97,62 @@ abstract class HtmlUtils $oBody = $oDoc->getElementsByTagName('body')->item(0); $aRemoveTags = array( - 'svg', 'link', 'base', 'meta', 'title', 'x-script', 'script', 'bgsound', 'keygen', 'source', + 'svg', 'link', 'base', 'meta', 'title', 'script', 'bgsound', 'keygen', 'source', 'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio', 'area', 'map', 'head', 'style' ); - - $aRemove = array(); - - $sIdRight = \md5(\microtime()); - - $aNodes = $oBody->getElementsByTagName('*'); - foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) - { - $sTagNameLower = \strtolower($oElement->nodeName); - - if (\in_array($sTagNameLower, $aRemoveTags)) - { - $aRemove[] = $oElement; - continue; - } - - // images - if ($oElement->hasAttribute('data-x-src-broken') || $oElement->hasAttribute('data-x-src-hidden')) { - $aRemove[] = $oElement; - continue; - } - if ($oElement->hasAttribute('data-x-src-cid')) { - $sCid = $oElement->getAttribute('data-x-src-cid'); - $oElement->removeAttribute('data-x-src-cid'); - if (!empty($sCid)) { - $aFoundCids[] = $sCid; - $oElement->setAttribute('src', 'cid:'.$sCid); + foreach ($aRemoveTags as $name) { + $aNodes = $oBody->getElementsByTagName($name); + foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) { + if (isset($oElement->parentNode)) { + @$oElement->parentNode->removeChild($oElement); } } - if ($oElement->hasAttribute('data-x-src')) { - $oElement->setAttribute('src', $oElement->getAttribute('data-x-src')); - $oElement->removeAttribute('data-x-src'); - } + } - // style attribute images - $aCid = array(); - if ($oElement->hasAttribute('data-x-style-url')) { - $aCid = \array_merge($aCid, \json_decode($oElement->getAttribute('data-x-style-url'), true)); - $oElement->removeAttribute('data-x-style-url'); + $xpath = new \DomXpath($oDoc); + + foreach ($xpath->query('//*[@data-x-src-broken]') as $oElement) { + if (isset($oElement->parentNode)) { + @$oElement->parentNode->removeChild($oElement); } + } + + foreach ($xpath->query('//*[@data-x-src-hidden]') as $oElement) { + if (isset($oElement->parentNode)) { + @$oElement->parentNode->removeChild($oElement); + } + } + + foreach ($xpath->query('//*[@data-x-src-cid]') as $oElement) { + $sCid = $oElement->getAttribute('data-x-src-cid'); + $oElement->removeAttribute('data-x-src-cid'); + if (!empty($sCid)) { + $aFoundCids[] = $sCid; + $oElement->setAttribute('src', 'cid:'.$sCid); + } + } + + foreach ($xpath->query('//*[@data-x-src]') as $oElement) { + $oElement->setAttribute('src', $oElement->getAttribute('data-x-src')); + $oElement->removeAttribute('data-x-src'); + } + + // style attribute images + foreach ($xpath->query('//*[@data-x-style-url]') as $oElement) { + $aCid = \json_decode($oElement->getAttribute('data-x-style-url'), true); + $oElement->removeAttribute('data-x-style-url'); if ($aCid) { foreach ($aCid as $sCidName => $sCid) { $sCidName = \strtolower(\preg_replace('/([A-Z])/', '-\1', $sCidName)); - if (\in_array($sCidName, array('background-image', 'list-style-image', 'content'))) - { + if (\in_array($sCidName, array('background-image', 'list-style-image', 'content'))) { $sStyles = $oElement->hasAttribute('style') ? \trim(\trim($oElement->getAttribute('style')), ';') : ''; $sBack = $sCidName.':url(cid:'.$sCid.')'; $sStyles = \preg_replace('/'.\preg_quote($sCidName).'\\s*:\\s*[^;]+/i', $sBack, $sStyles); - if (false === \strpos($sStyles, $sBack)) - { + if (false === \strpos($sStyles, $sBack)) { $sStyles .= ";{$sBack}"; } @@ -165,35 +161,28 @@ abstract class HtmlUtils } } } + } - // Remove all remaining data-* attributes + // Remove all remaining data-* attributes + foreach ($xpath->query('//*[@*[starts-with(name(), "data-")]]') as $oElement) { + $sTagNameLower = \strtolower($oElement->nodeName); if ($oElement->hasAttributes()) { foreach ($oElement->attributes as $oAttr) { - if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) - { + if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) { $oElement->removeAttribute($oAttr->nodeName); } } } - - if ('img' === $sTagNameLower) - { - $sSrc = $oElement->getAttribute('src'); - if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) - { - $sHash = \md5($sSrc) . '@' . $sIdRight; - $aFoundDataURL[$sHash] = $sSrc; - - $oElement->setAttribute('src', 'cid:'.$sHash); - } - } } - foreach ($aRemove as /* @var $oElement \DOMElement */ $oElement) - { - if (isset($oElement->parentNode)) - { - @$oElement->parentNode->removeChild($oElement); + $sIdRight = \md5(\microtime()); + $aNodes = $oBody->getElementsByTagName('img'); + foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) { + $sSrc = $oElement->getAttribute('src'); + if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) { + $sHash = \md5($sSrc) . '@' . $sIdRight; + $aFoundDataURL[$sHash] = $sSrc; + $oElement->setAttribute('src', 'cid:'.$sHash); } } @@ -201,87 +190,6 @@ abstract class HtmlUtils . $oDoc->saveHTML($oBody) . ''; } - public static function ConvertPlainToHtml(string $sText, bool $bLinksWithTargetBlank = true) : string - { - $sText = \trim($sText); - if (!\strlen($sText)) - { - return ''; - } - - $sText = (new \MailSo\Base\LinkFinder) - ->Text($sText) - ->UseDefaultWrappers($bLinksWithTargetBlank) - ->CompileText() - ; - - $sText = \str_replace("\r", '', $sText); - - $aText = \explode("\n", $sText); - unset($sText); - - $bIn = false; - $bDo = true; - do - { - $bDo = false; - $aNextText = array(); - foreach ($aText as $sTextLine) - { - $bStart = 0 === \strpos(\ltrim($sTextLine), '>'); - if ($bStart && !$bIn) - { - $bDo = true; - $bIn = true; - $aNextText[] = '
'; - $aNextText[] = \substr(\ltrim($sTextLine), 4); - } - else if (!$bStart && $bIn) - { - $bIn = false; - $aNextText[] = '
'; - $aNextText[] = $sTextLine; - } - else if ($bStart && $bIn) - { - $aNextText[] = \substr(\ltrim($sTextLine), 4); - } - else - { - $aNextText[] = $sTextLine; - } - } - - if ($bIn) - { - $bIn = false; - $aNextText[] = ''; - } - - $aText = $aNextText; - } - while ($bDo); - - $sText = \join("\n", $aText); - unset($aText); - - $sText = \preg_replace('/[\n][ ]+/', "\n", $sText); -// $sText = \preg_replace('/[\s]+([\s])/', '\\1', $sText); - - $sText = \preg_replace('/
[\s]+/i', '
', $sText); - $sText = \preg_replace('/[\s]+<\/blockquote>/i', '
', $sText); - - $sText = \preg_replace('/<\/blockquote>([\n]{0,2})
/i', '\\1', $sText); - $sText = \preg_replace('/[\n]{3,}/', "\n\n", $sText); - - $sText = \strtr($sText, array( - "\t" => "\xC2\xA0\xC2\xA0\xC2\xA0\xC2\xA0", - ' ' => "\xC2\xA0\xC2\xA0" - )); - - return \nl2br($sText); - } - public static function ConvertHtmlToPlain(string $sText) : string { $sText = \MailSo\Base\Utils::StripSpaces($sText); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php index 16ee13cfe..788932ae0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php @@ -23,8 +23,7 @@ class Http public static function SingletonInstance() : self { static $oInstance = null; - if (null === $oInstance) - { + if (null === $oInstance) { $oInstance = new self; } @@ -36,14 +35,14 @@ class Http * * @return mixed */ - public function GetServer(string $sKey, $mDefault = null) + public static function GetServer(string $sKey, $mDefault = null) { return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault; } public function GetMethod() : string { - return $this->GetServer('REQUEST_METHOD', ''); + return static::GetServer('REQUEST_METHOD', ''); } public function IsPost() : bool @@ -65,9 +64,8 @@ class Http public function IsLocalhost(string $sValueToCheck = '') : bool { - if (empty($sValueToCheck)) - { - $sValueToCheck = $this->GetServer('REMOTE_ADDR', ''); + if (empty($sValueToCheck)) { + $sValueToCheck = static::GetServer('REMOTE_ADDR', ''); } return $this->CheckLocalhost($sValueToCheck); @@ -76,29 +74,23 @@ class Http public function GetRawBody() : string { static $sRawBody = null; - if (null === $sRawBody) - { + if (null === $sRawBody) { $sBody = \file_get_contents('php://input'); $sRawBody = (false !== $sBody) ? $sBody : ''; } return $sRawBody; } - public function GetHeader(string $sHeader) : string + public static function GetHeader(string $sHeader) : string { $sServerKey = 'HTTP_'.\strtoupper(\str_replace('-', '_', $sHeader)); - $sResultHeader = $this->GetServer($sServerKey, ''); - - if (0 === \strlen($sResultHeader) && - \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) - { + $sResultHeader = static::GetServer($sServerKey, ''); + if (!\strlen($sResultHeader) && \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) { $sHeaders = \apache_request_headers(); - if (isset($sHeaders[$sHeader])) - { + if (isset($sHeaders[$sHeader])) { $sResultHeader = $sHeaders[$sHeader]; } } - return $sResultHeader; } @@ -109,155 +101,128 @@ class Http public function IsSecure(bool $bCheckProxy = true) : bool { - $sHttps = \strtolower($this->GetServer('HTTPS', '')); - if ('on' === $sHttps || ('' === $sHttps && '443' === (string) $this->GetServer('SERVER_PORT', ''))) - { - return true; - } - - if ($bCheckProxy && ( - ('https' === \strtolower($this->GetServer('HTTP_X_FORWARDED_PROTO', ''))) || - ('on' === \strtolower($this->GetServer('HTTP_X_FORWARDED_SSL', ''))) - )) - { - return true; - } - - return false; + $sHttps = \strtolower(static::GetServer('HTTPS', '')); + return ('on' === $sHttps || ('' === $sHttps && '443' === (string) static::GetServer('SERVER_PORT', ''))) + || ($bCheckProxy && ( + ('https' === \strtolower(static::GetServer('HTTP_X_FORWARDED_PROTO', ''))) || + ('on' === \strtolower(static::GetServer('HTTP_X_FORWARDED_SSL', ''))) + )); } - public function GetHost(bool $bWithRemoteUserData = false, bool $bWithoutWWW = true, bool $bWithoutPort = false) : string + public function GetHost(bool $bWithoutWWW = true, bool $bWithoutPort = false) : string { - $sHost = $this->GetServer('HTTP_HOST', ''); - if (!\strlen($sHost)) - { - $sName = $this->GetServer('SERVER_NAME'); - $iPort = (int) $this->GetServer('SERVER_PORT', 80); + $sHost = static::GetServer('HTTP_HOST', ''); + if (!\strlen($sHost)) { + $sName = static::GetServer('SERVER_NAME', ''); + $iPort = (int) static::GetServer('SERVER_PORT', 80); $sHost = (\in_array($iPort, array(80, 433))) ? $sName : $sName.':'.$iPort; } - if ($bWithoutWWW) - { - $sHost = 'www.' === \substr(\strtolower($sHost), 0, 4) ? \substr($sHost, 4) : $sHost; + if ($bWithoutWWW && 'www.' === \substr(\strtolower($sHost), 0, 4)) { + $sHost = \substr($sHost, 4); } - if ($bWithRemoteUserData) - { - $sUser = \trim($this->GetServer('REMOTE_USER', '')); - $sHost = (\strlen($sUser) ? $sUser.'@' : '').$sHost; - } - - if ($bWithoutPort) - { - $sHost = \preg_replace('/:\d+$/', '', $sHost); - } - - return $sHost; + return $bWithoutPort ? \preg_replace('/:\d+$/', '', $sHost) : $sHost; } public function GetClientIp(bool $bCheckProxy = false) : string { - $sIp = ''; - if ($bCheckProxy && null !== $this->GetServer('HTTP_CLIENT_IP', null)) - { - $sIp = $this->GetServer('HTTP_CLIENT_IP', ''); + if ($bCheckProxy && null !== static::GetServer('HTTP_CLIENT_IP', null)) { + return static::GetServer('HTTP_CLIENT_IP', ''); } - else if ($bCheckProxy && null !== $this->GetServer('HTTP_X_FORWARDED_FOR', null)) - { - $sIp = $this->GetServer('HTTP_X_FORWARDED_FOR', ''); + if ($bCheckProxy && null !== static::GetServer('HTTP_X_FORWARDED_FOR', null)) { + return static::GetServer('HTTP_X_FORWARDED_FOR', ''); } - else - { - $sIp = $this->GetServer('REMOTE_ADDR', ''); - } - - return $sIp; + return static::GetServer('REMOTE_ADDR', ''); } - public function ServerNotModifiedCache(int $iExpireTime, bool $bSetCacheHeader = true, string $sEtag = '') : bool + public static function checkETag(string $ETag) : void { - $bResult = false; - if (0 < $iExpireTime) - { - $iUtcTimeStamp = \time(); - $sIfModifiedSince = $this->GetHeader('If-Modified-Since', ''); - if (0 === \strlen($sIfModifiedSince)) - { - if ($bSetCacheHeader) - { - \header('Cache-Control: public'); - \header('Pragma: public'); - \header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iUtcTimeStamp - $iExpireTime).' UTC'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', $iUtcTimeStamp + $iExpireTime).' UTC'); - - if (\strlen($sEtag)) - { - \header('Etag: '.$sEtag); - } - } - } - else - { - static::StatusHeader(304); - $bResult = true; - } + // $ETag . APP_VERSION + $sIfNoneMatch = static::GetHeader('If-None-Match'); + if ($sIfNoneMatch && false !== \strpos($sIfNoneMatch, $ETag)) { + static::StatusHeader(304); + exit; + } + $sIfMatch = static::GetHeader('If-Match'); + if ($sIfMatch && false === \strpos($sIfMatch, $ETag)) { + static::StatusHeader(412); + exit; } - - return $bResult; } - /** - * @staticvar boolean $bCache - */ + public static function setETag(string $ETag) : void + { + // $ETag . APP_VERSION + static::checkETag($ETag); + \header("ETag: \"{$ETag}\""); + } + + public static function checkLastModified(int $mtime) : void + { + $sIfModifiedSince = static::GetHeader('If-Modified-Since'); + if ($sIfModifiedSince && $mtime <= \strtotime($sIfModifiedSince)) { + static::StatusHeader(304); + exit; + } + $sIfUnmodifiedSince = static::GetHeader('If-Unmodified-Since'); + if ($sIfUnmodifiedSince && $mtime > \strtotime($sIfUnmodifiedSince)) { + static::StatusHeader(412); + exit; + } + } + + public static function setLastModified(int $mtime) : void + { + static::checkLastModified($mtime); + \header('Last-Modified: '.\gmdate('D, d M Y H:i:s \G\M\T', $mtime)); # DATE_RFC1123 + } + + private static $bCache = false; + public function ServerNoCache() { - static $bCache = false; - if (false === $bCache) - { - $bCache = true; + if (!static::$bCache) { + static::$bCache = true; \header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); \header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT'); - \header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0'); + \header('Cache-Control: no-store'); \header('Pragma: no-cache'); } } - /** - * @staticvar boolean $bCache - */ - public function ServerUseCache(string $sEtag, int $iLastModified, int $iExpires) + public static function ServerUseCache(string $sEtag = '', int $iLastModified = 0, int $iExpires = 0) : void { - static $bCache = false; - if (false === $bCache) - { - $bCache = true; + if (!static::$bCache) { + static::$bCache = true; \header('Cache-Control: private'); - \header('ETag: '.$sEtag); - \header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iLastModified).' UTC'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', $iExpires).' UTC'); + $iExpires && \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + $iExpires).' UTC'); + $sEtag && static::setETag($sEtag); + $iLastModified && static::setLastModified($iLastModified); } } public static function StatusHeader(int $iStatus, string $sCustomStatusText = '') : void { - if (99 < $iStatus) - { + if (99 < $iStatus) { $aStatus = array( 200 => 'OK', 206 => 'Partial Content', 301 => 'Moved Permanently', + 302 => 'Found', 304 => 'Not Modified', 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', + 412 => 'Precondition Failed', 416 => 'Requested range not satisfiable', 500 => 'Internal Server Error' ); - $sHeaderText = (0 === \strlen($sCustomStatusText) && isset($aStatus[$iStatus]) ? $aStatus[$iStatus] : $sCustomStatusText); + $sHeaderText = (!\strlen($sCustomStatusText) && isset($aStatus[$iStatus]) ? $aStatus[$iStatus] : $sCustomStatusText); \http_response_code($iStatus); if (isset($_SERVER['SERVER_PROTOCOL'])) { @@ -269,9 +234,15 @@ class Http } } + public static function Location(string $sUrl, int $iStatus = 302): void + { + static::StatusHeader($iStatus); + \header('Location: ' . $sUrl); + } + public function GetPath() : string { - $sUrl = \ltrim(\substr($this->GetServer('SCRIPT_NAME', ''), 0, \strrpos($this->GetServer('SCRIPT_NAME', ''), '/')), '/'); + $sUrl = \ltrim(\substr(static::GetServer('SCRIPT_NAME', ''), 0, \strrpos(static::GetServer('SCRIPT_NAME', ''), '/')), '/'); return '' === $sUrl ? '/' : '/'.$sUrl.'/'; } @@ -282,6 +253,6 @@ class Http public function GetFullUrl() : string { - return $this->GetScheme().'://'.$this->GetHost(true, false).$this->GetPath(); + return $this->GetScheme().'://'.$this->GetHost(false).$this->GetPath(); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/LinkFinder.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/LinkFinder.php deleted file mode 100644 index d6286eb87..000000000 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/LinkFinder.php +++ /dev/null @@ -1,259 +0,0 @@ -iHtmlSpecialCharsFlags = (\defined('ENT_QUOTES') && \defined('ENT_SUBSTITUTE') && \defined('ENT_HTML401')) - ? ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 : ENT_QUOTES; - - if (\defined('ENT_IGNORE')) - { - $this->iHtmlSpecialCharsFlags |= ENT_IGNORE; - } - - $this->iOptimizationLimit = 300000; - - $this->Clear(); - } - - public function Clear() : self - { - $this->aPrepearPlainStringUrls = array(); - $this->fLinkWrapper = null; - $this->fMailWrapper = null; - $this->sText = ''; - - return $this; - } - - public function Text(string $sText) : self - { - $this->sText = $sText; - - return $this; - } - - /** - * @param mixed $fLinkWrapper - */ - public function LinkWrapper($fLinkWrapper) : self - { - $this->fLinkWrapper = $fLinkWrapper; - - return $this; - } - - /** - * @param mixed $fMailWrapper - */ - public function MailWrapper($fMailWrapper) : self - { - $this->fMailWrapper = $fMailWrapper; - - return $this; - } - - public function UseDefaultWrappers(bool $bAddTargetBlank = false) : self - { - $this->fLinkWrapper = function ($sLink) use ($bAddTargetBlank) { - - $sNameLink = $sLink; - if (!\preg_match('/^[a-z]{3,5}\:\/\//i', \ltrim($sLink))) - { - $sLink = 'https://'.\ltrim($sLink); - } - - return ''.$sNameLink.''; - }; - - $this->fMailWrapper = function ($sEmail) use ($bAddTargetBlank) { - return ''.$sEmail.''; - }; - - return $this; - } - - public function CompileText(bool $bUseHtmlSpecialChars = true) : string - { - $sText = \substr($this->sText, 0, $this->iOptimizationLimit); - $sSubText = \substr($this->sText, $this->iOptimizationLimit); - - $this->aPrepearPlainStringUrls = array(); - if (null !== $this->fLinkWrapper && \is_callable($this->fLinkWrapper)) - { - $sText = $this->findLinks($sText, $this->fLinkWrapper); - } - - if (null !== $this->fMailWrapper && \is_callable($this->fMailWrapper)) - { - $sText = $this->findMails($sText, $this->fMailWrapper); - } - - $sResult = ''; - if ($bUseHtmlSpecialChars) - { - $sResult = \htmlentities($sText.$sSubText, $this->iHtmlSpecialCharsFlags, 'UTF-8'); - } - else - { - $sResult = $sText.$sSubText; - } - - unset($sText, $sSubText); - - if (\count($this->aPrepearPlainStringUrls)) - { - $aPrepearPlainStringUrls = $this->aPrepearPlainStringUrls; - $sResult = \preg_replace_callback('/'.\preg_quote(static::OPEN_LINK, '/'). - '([\d]+)'.\preg_quote(static::CLOSE_LINK, '/').'/', - function ($aMatches) use ($aPrepearPlainStringUrls) { - $iIndex = (int) $aMatches[1]; - return isset($aPrepearPlainStringUrls[$iIndex]) ? $aPrepearPlainStringUrls[$iIndex] : ''; - }, $sResult); - - $this->aPrepearPlainStringUrls = array(); - } - - return $sResult; - } - - /** - * @param mixed $fWrapper - */ - private function findLinks(string $sText, $fWrapper) : string - { - $sPattern = '/([\W]|^)((?:https?:\/\/)|(?:svn:\/\/)|(?:git:\/\/)|(?:s?ftps?:\/\/)|(?:www\.))'. - '((\S+?)(\\/)?)((?:>)?|[^\w\=\\/;\(\)\[\]]*?)(?=<|\s|$)/imu'; - - $aPrepearPlainStringUrls = $this->aPrepearPlainStringUrls; - $sText = \preg_replace_callback($sPattern, function ($aMatch) use ($fWrapper, &$aPrepearPlainStringUrls) { - - if (\is_array($aMatch) && 6 < \count($aMatch)) - { - while (\in_array($sChar = \substr($aMatch[3], -1), array(']', ')'))) - { - if (\substr_count($aMatch[3], ']' === $sChar ? '[': '(') - \substr_count($aMatch[3], $sChar) < 0) - { - $aMatch[3] = \substr($aMatch[3], 0, -1); - $aMatch[6] = (']' === $sChar ? ']': ')').$aMatch[6]; - } - else - { - break; - } - } - - $sLinkWithWrap = $fWrapper($aMatch[2].$aMatch[3]); - if (\is_string($sLinkWithWrap) && \strlen($sLinkWithWrap)) - { - $aPrepearPlainStringUrls[] = \stripslashes($sLinkWithWrap); - return $aMatch[1]. - static::OPEN_LINK. - (\count($aPrepearPlainStringUrls) - 1). - static::CLOSE_LINK. - $aMatch[6]; - } - - return $aMatch[0]; - } - - return ''; - - }, $sText); - - if (\count($aPrepearPlainStringUrls)) - { - $this->aPrepearPlainStringUrls = $aPrepearPlainStringUrls; - } - - return $sText; - } - - /** - * @param mixed $fWrapper - */ - private function findMails(string $sText, $fWrapper) : string - { - $sPattern = '/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/'; - - $aPrepearPlainStringUrls = $this->aPrepearPlainStringUrls; - $sText = \preg_replace_callback($sPattern, function ($aMatch) use ($fWrapper, &$aPrepearPlainStringUrls) { - - if (\is_array($aMatch) && isset($aMatch[1])) - { - $sMailWithWrap = $fWrapper($aMatch[1]); - if (\is_string($sMailWithWrap) && \strlen($sMailWithWrap)) - { - $aPrepearPlainStringUrls[] = \stripslashes($sMailWithWrap); - return static::OPEN_LINK. - (\count($aPrepearPlainStringUrls) - 1). - static::CLOSE_LINK; - } - - return $aMatch[1]; - } - - return ''; - - }, $sText); - - if (\count($aPrepearPlainStringUrls)) - { - $this->aPrepearPlainStringUrls = $aPrepearPlainStringUrls; - } - - return $sText; - } -} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php index abf864be5..75d3c926d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/ResourceRegistry.php @@ -17,10 +17,7 @@ namespace MailSo\Base; */ abstract class ResourceRegistry { - /** - * @var array - */ - public static $Resources = array(); + public static array $Resources = array(); /** * @staticvar bool $bInited @@ -28,16 +25,12 @@ abstract class ResourceRegistry private static function regResourcesShutdownFunc() : void { static $bInited = false; - if (!$bInited) - { + if (!$bInited) { $bInited = true; \register_shutdown_function(function () { - if (\is_array(static::$Resources)) - { - foreach (\array_keys(static::$Resources) as $sKey) - { - if (\is_resource(static::$Resources[$sKey])) - { + if (\is_array(static::$Resources)) { + foreach (\array_keys(static::$Resources) as $sKey) { + if (\is_resource(static::$Resources[$sKey])) { \fclose(static::$Resources[$sKey]); } static::$Resources[$sKey] = null; @@ -57,8 +50,7 @@ abstract class ResourceRegistry self::regResourcesShutdownFunc(); $oResult = \fopen('php://temp/maxmemory:'.($iMemoryMaxInMb * 1024 * 1024), 'r+b'); - if (\is_resource($oResult)) - { + if (\is_resource($oResult)) { static::$Resources[(string) $oResult] = $oResult; return $oResult; } @@ -72,8 +64,7 @@ abstract class ResourceRegistry public static function CreateMemoryResourceFromString(string $sString) { $oResult = self::CreateMemoryResource(); - if (\is_resource($oResult)) - { + if (\is_resource($oResult)) { \fwrite($oResult, $sString); \rewind($oResult); } @@ -86,18 +77,15 @@ abstract class ResourceRegistry */ public static function CloseMemoryResource(&$rResource) : void { - if (\is_resource($rResource)) - { + if (\is_resource($rResource)) { $sKey = (string) $rResource; - if (isset(static::$Resources[$sKey])) - { + if (isset(static::$Resources[$sKey])) { \fclose(static::$Resources[$sKey]); static::$Resources[$sKey] = null; unset(static::$Resources[$sKey]); } - if (\is_resource($rResource)) - { + if (\is_resource($rResource)) { \fclose($rResource); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php index 3c9c93a73..a1101f594 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Binary.php @@ -19,55 +19,34 @@ namespace MailSo\Base\StreamWrappers; */ class Binary { + /** @var resource|null */ + public $context; + /** * @var string */ const STREAM_NAME = 'mailsobinary'; - /** - * @var array - */ - private static $aStreams = array(); + private static array $aStreams = array(); - /** - * @var array - */ - private static $aRememberStreams = array(); + private static array $aRememberStreams = array(); /** * @var resource */ private $rStream; - /** - * @var string - */ - private $sFromEncoding; + private string $sFromEncoding; - /** - * @var string - */ - private $sToEncoding; + private string $sToEncoding; - /** - * @var string - */ - private $sFunctionName; + private string $sFunctionName; - /** - * @var int - */ - private $iPos; + private int $iPos; - /** - * @var string - */ - private $sBuffer; + private string $sBuffer; - /** - * @var string - */ - private $sReadEndBuffer; + private string $sReadEndBuffer; public static function GetInlineDecodeOrEncodeFunctionName(string $sContentTransferEncoding, bool $bDecode = true) : string { @@ -93,8 +72,7 @@ class Binary $sEndBuffer = ''; $iQuotedPrintableLen = \strlen($sEncodedString); $iLastSpace = \strrpos($sEncodedString, ' '); - if (false !== $iLastSpace && $iLastSpace + 1 < $iQuotedPrintableLen) - { + if (false !== $iLastSpace && $iLastSpace + 1 < $iQuotedPrintableLen) { $sEndBuffer = \substr($sEncodedString, $iLastSpace + 1); $sEncodedString = \substr($sEncodedString, 0, $iLastSpace + 1); } @@ -106,10 +84,8 @@ class Binary */ public static function IsStreamRemembed($rStream) : bool { - foreach (self::$aRememberStreams as $rRem) - { - if ($rStream === $rRem) - { + foreach (self::$aRememberStreams as $rRem) { + if ($rStream === $rRem) { return true; } } @@ -122,8 +98,7 @@ class Binary */ public static function RememberStream($rStream) { - if (!self::IsStreamRemembed($rStream)) - { + if (!self::IsStreamRemembed($rStream)) { self::$aRememberStreams[] = $rStream; } } @@ -136,15 +111,13 @@ class Binary public static function CreateStream($rStream, string $sUtilsDecodeOrEncodeFunctionName = null, string $sFromEncoding = null, string $sToEncoding = null) { - if (null === $sUtilsDecodeOrEncodeFunctionName || !\strlen($sUtilsDecodeOrEncodeFunctionName)) - { + if (null === $sUtilsDecodeOrEncodeFunctionName || !\strlen($sUtilsDecodeOrEncodeFunctionName)) { $sUtilsDecodeOrEncodeFunctionName = 'InlineNullDecode'; } $sHashName = \md5(\microtime(true).\rand(1000, 9999)); - if (null !== $sFromEncoding && null !== $sToEncoding && $sFromEncoding !== $sToEncoding) - { + if (null !== $sFromEncoding && null !== $sToEncoding && $sFromEncoding !== $sToEncoding) { $rStream = self::CreateStream($rStream, $sUtilsDecodeOrEncodeFunctionName); $sUtilsDecodeOrEncodeFunctionName = 'InlineConvertDecode'; } @@ -213,27 +186,19 @@ class Binary $sReturn = ''; $sFunctionName = $this->sFunctionName; - if ($iCount > 0) - { - if ($iCount < \strlen($this->sBuffer)) - { + if ($iCount > 0) { + if ($iCount < \strlen($this->sBuffer)) { $sReturn = \substr($this->sBuffer, 0, $iCount); $this->sBuffer = \substr($this->sBuffer, $iCount); - } - else - { + } else { $sReturn = $this->sBuffer; - while ($iCount > 0) - { - if (\feof($this->rStream)) - { - if (!\strlen($this->sBuffer.$sReturn)) - { + while ($iCount > 0) { + if (\feof($this->rStream)) { + if (!\strlen($this->sBuffer.$sReturn)) { return false; } - if (\strlen($this->sReadEndBuffer)) - { + if (\strlen($this->sReadEndBuffer)) { $sReturn .= self::$sFunctionName($this->sReadEndBuffer, $this->sReadEndBuffer, $this->sFromEncoding, $this->sToEncoding); @@ -242,12 +207,9 @@ class Binary $iCount = 0; $this->sBuffer = ''; - } - else - { + } else { $sReadResult = \fread($this->rStream, 8192); - if (false === $sReadResult) - { + if (false === $sReadResult) { return false; } @@ -255,14 +217,11 @@ class Binary $this->sReadEndBuffer, $this->sFromEncoding, $this->sToEncoding); $iDecodeLen = \strlen($sReadResult); - if ($iCount < $iDecodeLen) - { + if ($iCount < $iDecodeLen) { $this->sBuffer = \substr($sReturn, $iCount); $sReturn = \substr($sReturn, 0, $iCount); $iCount = 0; - } - else - { + } else { $iCount -= $iDecodeLen; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php index 1f4d3d1ec..f9ab9c645 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/Literal.php @@ -18,30 +18,24 @@ namespace MailSo\Base\StreamWrappers; */ class Literal { + /** @var resource|null */ + public $context; + /** * @var string */ const STREAM_NAME = 'mailsoliteral'; - /** - * @var array - */ - private static $aStreams = array(); + private static array $aStreams = array(); /** * @var resource */ private $rStream; - /** - * @var int - */ - private $iSize; + private int $iSize; - /** - * @var int - */ - private $iPos; + private int $iPos; /** * @param resource $rStream @@ -92,20 +86,16 @@ class Literal public function stream_read(int $iCount) : string { $sResult = false; - if ($this->iSize < $this->iPos + $iCount) - { + if ($this->iSize < $this->iPos + $iCount) { $iCount = $this->iSize - $this->iPos; } - if ($iCount > 0) - { + if ($iCount > 0) { $sReadResult = ''; $iRead = $iCount; - while (0 < $iRead) - { + while (0 < $iRead) { $sAddRead = \fread($this->rStream, $iRead); - if (false === $sAddRead) - { + if (false === $sAddRead) { $sReadResult = false; break; } @@ -115,8 +105,7 @@ class Literal $this->iPos += \strlen($sAddRead); } - if (false !== $sReadResult) - { + if (false !== $sReadResult) { $sResult = $sReadResult; } } @@ -160,6 +149,8 @@ class Literal public function stream_seek() : bool { +// $this->iPos = $offset; +// \fseek($this->rStream, $offset, $whence); return false; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php index be64df4cd..267a0240d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/SubStreams.php @@ -19,40 +19,25 @@ namespace MailSo\Base\StreamWrappers; */ class SubStreams { + /** @var resource|null */ + public $context; + /** * @var string */ const STREAM_NAME = 'mailsosubstreams'; - /** - * @var array - */ - private static $aStreams = array(); + private static array $aStreams = array(); - /** - * @var array - */ - private $aSubStreams; + private array $aSubStreams; - /** - * @var int - */ - private $iIndex; + private int $iIndex; - /** - * @var string - */ - private $sBuffer; + private string $sBuffer; - /** - * @var bool - */ - private $bIsEnd; + private bool $bIsEnd; - /** - * @var int - */ - private $iPos; + private int $iPos; /** * @return resource|bool @@ -69,20 +54,6 @@ class SubStreams return \fopen(self::STREAM_NAME.'://'.$sHashName, 'rb'); } - /** - * @return resource|null - */ - protected function &getPart() - { - $nNull = null; - if (isset($this->aSubStreams[$this->iIndex])) - { - return $this->aSubStreams[$this->iIndex]; - } - - return $nNull; - } - public function stream_open(string $sPath) : bool { $this->aSubStreams = array(); @@ -116,53 +87,41 @@ class SubStreams { $sReturn = ''; $mCurrentPart = null; - if ($iCount > 0) - { - if ($iCount < \strlen($this->sBuffer)) - { + if ($iCount > 0) { + if ($iCount < \strlen($this->sBuffer)) { $sReturn = \substr($this->sBuffer, 0, $iCount); $this->sBuffer = \substr($this->sBuffer, $iCount); - } - else - { + } else { $sReturn = $this->sBuffer; - while ($iCount > 0) - { - $mCurrentPart =& $this->getPart(); - if (null === $mCurrentPart) - { + while ($iCount > 0) { + $mCurrentPart = isset($this->aSubStreams[$this->iIndex]) + ? $this->aSubStreams[$this->iIndex] + : null; + if (null === $mCurrentPart) { $this->bIsEnd = true; $this->sBuffer = ''; $iCount = 0; break; } - if (\is_resource($mCurrentPart)) - { - if (!\feof($mCurrentPart)) - { + if (\is_resource($mCurrentPart)) { + if (!\feof($mCurrentPart)) { $sReadResult = \fread($mCurrentPart, 8192); - if (false === $sReadResult) - { + if (false === $sReadResult) { return false; } $sReturn .= $sReadResult; - } - else - { - $this->iIndex++; + } else { + ++$this->iIndex; } } $iLen = \strlen($sReturn); - if ($iCount < $iLen) - { + if ($iCount < $iLen) { $this->sBuffer = \substr($sReturn, $iCount); $sReturn = \substr($sReturn, 0, $iCount); $iCount = 0; - } - else - { + } else { $iCount -= $iLen; } } @@ -211,6 +170,8 @@ class SubStreams public function stream_seek() : bool { +// $this->iPos = $offset; +// foreach ($this->aSubStreams as $rStream) \fseek($rStream, $offset, $whence); return false; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php index 12a015a2b..a1e6e7e60 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/StreamWrappers/TempFile.php @@ -18,6 +18,9 @@ namespace MailSo\Base\StreamWrappers; */ class TempFile { + /** @var resource|null */ + public $context; + /** * @var string */ @@ -56,15 +59,11 @@ class TempFile self::STREAM_NAME === $aPath['scheme']) { $sHashName = $aPath['host']; - if (isset(self::$aStreams[$sHashName]) && - \is_resource(self::$aStreams[$sHashName])) - { + if (isset(self::$aStreams[$sHashName]) && \is_resource(self::$aStreams[$sHashName])) { $this->rStream = self::$aStreams[$sHashName]; \fseek($this->rStream, 0); $bResult = true; - } - else - { + } else { $this->rStream = \fopen('php://temp', 'r+b'); self::$aStreams[$sHashName] = $this->rStream; 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 aedc2625b..be1da5ff9 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 @@ -190,8 +190,7 @@ abstract class Utils public static function FormatFileSize(int $iSize, int $iRound = 0) : string { $aSizes = array('B', 'KB', 'MB'); - for ($iIndex = 0; $iSize > 1024 && isset($aSizes[$iIndex + 1]); $iIndex++) - { + for ($iIndex = 0; $iSize > 1024 && isset($aSizes[$iIndex + 1]); ++$iIndex) { $iSize /= 1024; } return \round($iSize, $iRound).$aSizes[$iIndex]; @@ -223,12 +222,10 @@ abstract class Utils public static function DecodeHeaderValue(string $sEncodedValue, string $sIncomingCharset = '', string $sForcedIncomingCharset = '') : string { $sValue = $sEncodedValue; - if (\strlen($sIncomingCharset)) - { + if (\strlen($sIncomingCharset)) { $sIncomingCharset = static::NormalizeCharsetByValue($sIncomingCharset, $sValue); - $sValue = static::ConvertEncoding($sValue, $sIncomingCharset, - Enumerations\Charset::UTF_8); + $sValue = static::ConvertEncoding($sValue, $sIncomingCharset, Enumerations\Charset::UTF_8); } $sValue = \preg_replace('/\?=[\n\r\t\s]{1,5}=\?/m', '?==?', $sValue); @@ -239,15 +236,11 @@ abstract class Utils // \preg_match_all('/=\?[^\?]+\?[q|b|Q|B]\?[^\?]*?\?=/', $sValue, $aMatch); \preg_match_all('/=\?[^\?]+\?[q|b|Q|B]\?.*?\?=/', $sValue, $aMatch); - if (isset($aMatch[0]) && \is_array($aMatch[0])) - { - for ($iIndex = 0, $iLen = \count($aMatch[0]); $iIndex < $iLen; $iIndex++) - { - if (isset($aMatch[0][$iIndex])) - { + if (isset($aMatch[0]) && \is_array($aMatch[0])) { + for ($iIndex = 0, $iLen = \count($aMatch[0]); $iIndex < $iLen; ++$iIndex) { + if (isset($aMatch[0][$iIndex])) { $iPos = \strpos($aMatch[0][$iIndex], '*'); - if (false !== $iPos) - { + if (false !== $iPos) { $aMatch[0][$iIndex][0] = \substr($aMatch[0][$iIndex][0], 0, $iPos); } } @@ -261,11 +254,9 @@ abstract class Utils $sMainCharset = ''; $bOneCharset = true; - for ($iIndex = 0, $iLen = \count($aEncodeArray); $iIndex < $iLen; $iIndex++) - { + for ($iIndex = 0, $iLen = \count($aEncodeArray); $iIndex < $iLen; ++$iIndex) { $aTempArr = array('', $aEncodeArray[$iIndex]); - if ('=?' === \substr(\trim($aTempArr[1]), 0, 2)) - { + if ('=?' === \substr(\trim($aTempArr[1]), 0, 2)) { $iPos = \strpos($aTempArr[1], '?', 2); $aTempArr[0] = \substr($aTempArr[1], 2, $iPos - 2); $sEncType = \strtoupper($aTempArr[1][$iPos + 1]); @@ -284,17 +275,13 @@ abstract class Utils } } - if (\strlen($aTempArr[0])) - { + if (\strlen($aTempArr[0])) { $sCharset = \strlen($sForcedIncomingCharset) ? $sForcedIncomingCharset : $aTempArr[0]; $sCharset = static::NormalizeCharset($sCharset, true); - if ('' === $sMainCharset) - { + if ('' === $sMainCharset) { $sMainCharset = $sCharset; - } - else if ($sMainCharset !== $sCharset) - { + } else if ($sMainCharset !== $sCharset) { $bOneCharset = false; } } @@ -308,14 +295,10 @@ abstract class Utils unset($aTempArr); } - for ($iIndex = 0, $iLen = \count($aParts); $iIndex < $iLen; $iIndex++) - { - if ($bOneCharset) - { + for ($iIndex = 0, $iLen = \count($aParts); $iIndex < $iLen; ++$iIndex) { + if ($bOneCharset) { $sValue = \str_replace($aParts[$iIndex][0], $aParts[$iIndex][1], $sValue); - } - else - { + } else { $aParts[$iIndex][2] = static::NormalizeCharsetByValue($aParts[$iIndex][2], $aParts[$iIndex][1]); $sValue = \str_replace($aParts[$iIndex][0], @@ -324,8 +307,7 @@ abstract class Utils } } - if ($bOneCharset && \strlen($sMainCharset)) - { + if ($bOneCharset && \strlen($sMainCharset)) { $sMainCharset = static::NormalizeCharsetByValue($sMainCharset, $sValue); $sValue = static::ConvertEncoding($sValue, $sMainCharset, Enumerations\Charset::UTF_8); } @@ -337,8 +319,7 @@ abstract class Utils { $sResultHeaders = $sIncHeaders; - if ($aHeadersToRemove) - { + if ($aHeadersToRemove) { $aHeadersToRemove = \array_map('strtolower', $aHeadersToRemove); $sIncHeaders = \preg_replace('/[\r\n]+/', "\n", $sIncHeaders); @@ -347,31 +328,21 @@ abstract class Utils $bSkip = false; $aResult = array(); - foreach ($aHeaders as $sLine) - { - if (\strlen($sLine)) - { + foreach ($aHeaders as $sLine) { + if (\strlen($sLine)) { $sFirst = \substr($sLine,0,1); - if (' ' === $sFirst || "\t" === $sFirst) - { - if (!$bSkip) - { + if (' ' === $sFirst || "\t" === $sFirst) { + if (!$bSkip) { $aResult[] = $sLine; } - } - else - { + } else { $bSkip = false; $aParts = \explode(':', $sLine, 2); - if (!empty($aParts) && !empty($aParts[0])) - { - if (\in_array(\strtolower(\trim($aParts[0])), $aHeadersToRemove)) - { + if (!empty($aParts) && !empty($aParts[0])) { + if (\in_array(\strtolower(\trim($aParts[0])), $aHeadersToRemove)) { $bSkip = true; - } - else - { + } else { $aResult[] = $sLine; } } @@ -388,20 +359,17 @@ abstract class Utils public static function EncodeUnencodedValue(string $sEncodeType, string $sValue) : string { $sValue = \trim($sValue); - if (\strlen($sValue) && !static::IsAscii($sValue)) - { + if (\strlen($sValue) && !static::IsAscii($sValue)) { switch (\strtoupper($sEncodeType)) { case 'B': - $sValue = '=?'.\strtolower(Enumerations\Charset::UTF_8). + return '=?'.\strtolower(Enumerations\Charset::UTF_8). '?B?'.\base64_encode($sValue).'?='; - break; case 'Q': - $sValue = '=?'.\strtolower(Enumerations\Charset::UTF_8). + return '=?'.\strtolower(Enumerations\Charset::UTF_8). '?Q?'.\str_replace(array('?', ' ', '_'), array('=3F', '_', '=5F'), \quoted_printable_encode($sValue)).'?='; - break; } } @@ -418,28 +386,22 @@ abstract class Utils $iNlen = \strlen($sAttrName); $iVlen = \strlen($sValue); - if (\strlen($sAttrName) + $iVlen > $iLen - 3) - { + if (\strlen($sAttrName) + $iVlen > $iLen - 3) { $sections = array(); $section = 0; - for ($i = 0, $j = 0; $i < $iVlen; $i += $j) - { + for ($i = 0, $j = 0; $i < $iVlen; $i += $j) { $j = $iLen - $iNlen - \strlen($section) - 4; $sections[$section++] = \substr($sValue, $i, $j); } - for ($i = 0, $n = $section; $i < $n; $i++) - { + for ($i = 0, $n = $section; $i < $n; ++$i) { $sections[$i] = ' '.$sAttrName.'*'.$i.'*='.$sections[$i]; } return \implode(";\r\n", $sections); } - else - { - return $sAttrName.'*='.$sValue; - } + return $sAttrName.'*='.$sValue; } public static function EncodeHeaderUtf8AttributeValue(string $sAttrName, string $sValue) : string @@ -447,12 +409,9 @@ abstract class Utils $sAttrName = \trim($sAttrName); $sValue = \trim($sValue); - if (\strlen($sValue) && !static::IsAscii($sValue)) - { + if (\strlen($sValue) && !static::IsAscii($sValue)) { $sValue = static::AttributeRfc2231Encode($sAttrName, $sValue); - } - else - { + } else { $sValue = $sAttrName.'="'.\str_replace('"', '\\"', $sValue).'"'; } @@ -462,8 +421,7 @@ abstract class Utils public static function GetAccountNameFromEmail(string $sEmail) : string { $sResult = ''; - if (\strlen($sEmail)) - { + if (\strlen($sEmail)) { $iPos = \strrpos($sEmail, '@'); $sResult = (false === $iPos) ? $sEmail : \substr($sEmail, 0, $iPos); } @@ -474,11 +432,9 @@ abstract class Utils public static function GetDomainFromEmail(string $sEmail) : string { $sResult = ''; - if (\strlen($sEmail)) - { + if (\strlen($sEmail)) { $iPos = \strrpos($sEmail, '@'); - if (false !== $iPos && 0 < $iPos) - { + if (false !== $iPos && 0 < $iPos) { $sResult = \substr($sEmail, $iPos + 1); } } @@ -501,243 +457,13 @@ abstract class Utils return false === $iLast ? '' : \strtolower(\substr($sFileName, $iLast + 1)); } - public static function MimeContentType(string $sFileName) : string - { - $sResult = 'application/octet-stream'; - $sFileName = \trim(\strtolower($sFileName)); - - if ('winmail.dat' === $sFileName) - { - return 'application/ms-tnef'; - } - - $aMimeTypes = array( - - 'eml' => 'message/rfc822', - 'mime' => 'message/rfc822', - 'txt' => 'text/plain', - 'text' => 'text/plain', - 'def' => 'text/plain', - 'list' => 'text/plain', - 'in' => 'text/plain', - 'ini' => 'text/plain', - 'log' => 'text/plain', - 'sql' => 'text/plain', - 'cfg' => 'text/plain', - 'conf' => 'text/plain', - 'rtx' => 'text/richtext', - 'vcard' => 'text/vcard', - 'vcf' => 'text/vcard', - 'htm' => 'text/html', - 'html' => 'text/html', - 'csv' => 'text/csv', - 'ics' => 'text/calendar', - 'ifb' => 'text/calendar', - 'xml' => 'text/xml', - 'json' => 'application/json', - 'swf' => 'application/x-shockwave-flash', - 'hlp' => 'application/winhlp', - 'wgt' => 'application/widget', - 'chm' => 'application/vnd.ms-htmlhelp', - 'asc' => 'application/pgp-signature', - 'p10' => 'application/pkcs10', - 'p7c' => 'application/pkcs7-mime', - 'p7m' => 'application/pkcs7-mime', - 'p7s' => 'application/pkcs7-signature', - 'torrent' => 'application/x-bittorrent', - - // scripts - 'js' => 'application/javascript', - 'pl' => 'text/perl', - 'css' => 'text/css', - 'asp' => 'text/asp', - 'php' => 'application/x-php', - - // images - 'png' => 'image/png', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpe' => 'image/jpeg', - 'jfif' => 'image/jpeg', - 'gif' => 'image/gif', - 'bmp' => 'image/bmp', - 'cgm' => 'image/cgm', - 'ief' => 'image/ief', - 'ico' => 'image/x-icon', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'svg' => 'image/svg+xml', - 'svgz' => 'image/svg+xml', - 'djv' => 'image/vnd.djvu', - 'djvu' => 'image/vnd.djvu', - 'webp' => 'image/webp', - - // archives - 'zip' => 'application/zip', - '7z' => 'application/x-7z-compressed', - 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', - 'dll' => 'application/x-msdownload', - 'scr' => 'application/x-msdownload', - 'com' => 'application/x-msdownload', - 'bat' => 'application/x-msdownload', - 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', - 'gz' => 'application/x-gzip', - 'tgz' => 'application/x-gzip', - 'bz' => 'application/x-bzip', - 'bz2' => 'application/x-bzip2', - 'deb' => 'application/x-debian-package', - 'tar' => 'application/x-tar', - - // fonts - 'psf' => 'application/x-font-linux-psf', - 'otf' => 'application/x-font-otf', - 'pcf' => 'application/x-font-pcf', - 'snf' => 'application/x-font-snf', - 'ttf' => 'application/x-font-ttf', - 'ttc' => 'application/x-font-ttf', - - // audio - 'aac' => 'audio/aac', - 'flac' => 'audio/flac', - 'mp3' => 'audio/mpeg', - 'aif' => 'audio/aiff', - 'aifc' => 'audio/aiff', - 'aiff' => 'audio/aiff', - 'wav' => 'audio/x-wav', - 'midi' => 'audio/midi', - 'mp4a' => 'audio/mp4', - 'ogg' => 'audio/ogg', - 'weba' => 'audio/webm', - 'm3u' => 'audio/x-mpegurl', - - // video - 'qt' => 'video/quicktime', - 'mov' => 'video/quicktime', - 'avi' => 'video/x-msvideo', - 'mpg' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpe' => 'video/mpeg', - 'm1v' => 'video/mpeg', - 'm2v' => 'video/mpeg', - '3gp' => 'video/3gpp', - '3g2' => 'video/3gpp2', - 'h261' => 'video/h261', - 'h263' => 'video/h263', - 'h264' => 'video/h264', - 'jpgv' => 'video/jpgv', - 'mp4' => 'video/mp4', - 'mp4v' => 'video/mp4', - 'mpg4' => 'video/mp4', - 'ogv' => 'video/ogg', - 'webm' => 'video/webm', - 'm4v' => 'video/x-m4v', - 'asf' => 'video/x-ms-asf', - 'asx' => 'video/x-ms-asf', - 'wm' => 'video/x-ms-wm', - 'wmv' => 'video/x-ms-wmv', - 'wmx' => 'video/x-ms-wmx', - 'wvx' => 'video/x-ms-wvx', - 'movie' => 'video/x-sgi-movie', - - // adobe - 'pdf' => 'application/pdf', - 'psd' => 'image/vnd.adobe.photoshop', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - - // ms office - 'doc' => 'application/msword', - 'dot' => 'application/msword', - 'rtf' => 'application/rtf', - 'xls' => 'application/vnd.ms-excel', - 'ppt' => 'application/vnd.ms-powerpoint', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - - // open office - 'odt' => 'application/vnd.oasis.opendocument.text', - 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', - 'odp' => 'application/vnd.oasis.opendocument.presentation' - - ); - - $sExt = static::GetFileExtension($sFileName); - if (\strlen($sExt) && isset($aMimeTypes[$sExt])) - { - $sResult = $aMimeTypes[$sExt]; - } - - return $sResult; - } - - public static function ContentTypeType(string $sContentType, string $sFileName) : string - { - $sContentType = \strtolower($sContentType); - if (\str_starts_with($sContentType, 'image/')) { - return 'image'; - } - - switch ($sContentType) - { - case 'application/zip': - case 'application/x-7z-compressed': - case 'application/x-rar-compressed': - case 'application/x-msdownload': - case 'application/vnd.ms-cab-compressed': - case 'application/gzip': - case 'application/x-gzip': - case 'application/x-bzip': - case 'application/x-bzip2': - case 'application/x-debian-package': - case 'application/x-tar': - return 'archive'; - - case 'application/msword': - case 'application/rtf': - case 'application/vnd.ms-excel': - case 'application/vnd.ms-powerpoint': - case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': - case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': - case 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': - case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': - case 'application/vnd.oasis.opendocument.text': - case 'application/vnd.oasis.opendocument.spreadsheet': - return 'doc'; - - case 'application/pdf': - case 'application/x-pdf': - return 'pdf'; - } - - switch (\strtolower(static::GetFileExtension($sFileName))) - { - case 'zip': - case '7z': - case 'rar': - case 'tar': - case 'tgz': - return 'archive'; - - case 'pdf': - return 'pdf'; - } - - return ''; - } - /** * @staticvar bool $bValidateAction */ public static function ResetTimeLimit(int $iTimeToReset = 15, int $iTimeToAdd = 120) : bool { $iTime = \time(); - if ($iTime < $_SERVER['REQUEST_TIME_FLOAT'] + 5) - { + if ($iTime < $_SERVER['REQUEST_TIME_FLOAT'] + 5) { // do nothing first 5s return true; } @@ -745,18 +471,15 @@ abstract class Utils static $bValidateAction = null; static $iResetTimer = null; - if (null === $bValidateAction) - { + if (null === $bValidateAction) { $iResetTimer = 0; $bValidateAction = static::FunctionCallable('set_time_limit'); } - if ($bValidateAction && $iTimeToReset < $iTime - $iResetTimer) - { + if ($bValidateAction && $iTimeToReset < $iTime - $iResetTimer) { $iResetTimer = $iTime; - if (!\set_time_limit($iTimeToAdd)) - { + if (!\set_time_limit($iTimeToAdd)) { $bValidateAction = false; return false; } @@ -767,11 +490,13 @@ abstract class Utils return false; } - # Replace ampersand, spaces and reserved characters (based on Win95 VFAT) - # en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + /** + * Replace control characters, ampersand and reserved characters (based on Win95 VFAT) + * en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + */ public static function SecureFileName(string $sFileName) : string { - return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $sFileName); + return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\pC]#su', '-', $sFileName); } public static function Trim(string $sValue) : string @@ -781,24 +506,7 @@ abstract class Utils public static function RecRmDir(string $sDir) : bool { - \clearstatcache(); - if (\is_dir($sDir)) { - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($sDir, \FilesystemIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST); - foreach ($iterator as $path) { - if ($path->isDir()) { - \rmdir($path); - } else { - \unlink($path); - } - } - \clearstatcache(); -// \realpath_cache_size() && \clearstatcache(true); - return \rmdir($sDir); - } - - return false; + return static::RecTimeDirRemove($sDir, 0); } public static function RecTimeDirRemove(string $sDir, int $iTime2Kill) : bool @@ -810,9 +518,10 @@ abstract class Utils new \RecursiveDirectoryIterator($sDir, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST); foreach ($iterator as $path) { - if ($path->isFile() && $path->getMTime() < $iTime) { + if ($path->isFile() && (!$iTime2Kill || $path->getMTime() < $iTime)) { + \is_callable('opcache_invalidate') && \opcache_invalidate($path, true); \unlink($path); - } else if ($path->isDir() && !(new \FilesystemIterator($path))->valid()) { + } else if ($path->isDir() && (!$iTime2Kill || !(new \FilesystemIterator($path))->valid())) { \rmdir($path); } } @@ -848,24 +557,19 @@ abstract class Utils public static function Base64Decode(string $sString) : string { $sResultString = \base64_decode($sString, true); - if (false === $sResultString) - { + if (false === $sResultString) { $sString = \str_replace(array(' ', "\r", "\n", "\t"), '', $sString); $sString = \preg_replace('/[^a-zA-Z0-9=+\/](.*)$/', '', $sString); - if (false !== \strpos(\trim(\trim($sString), '='), '=')) - { + if (false !== \strpos(\trim(\trim($sString), '='), '=')) { $sString = \preg_replace('/=([^=])/', '= $1', $sString); $aStrings = \explode(' ', $sString); - foreach ($aStrings as $iIndex => $sParts) - { + foreach ($aStrings as $iIndex => $sParts) { $aStrings[$iIndex] = \base64_decode($sParts); } $sResultString = \implode('', $aStrings); - } - else - { + } else { $sResultString = \base64_decode($sString); } } @@ -880,7 +584,7 @@ abstract class Utils public static function UrlSafeBase64Decode(string $sValue) : string { - return \base64_decode(\strtr($sValue, '-_', '+/'), '='); + return \base64_decode(\strtr($sValue, '-_', '+/'), true); } /** @@ -888,23 +592,16 @@ abstract class Utils */ public static function FpassthruWithTimeLimitReset($fResource, int $iBufferLen = 8192) : bool { - $bResult = false; - if (\is_resource($fResource)) - { - while (!\feof($fResource)) - { + $bResult = \is_resource($fResource); + if ($bResult) { + while (!\feof($fResource)) { $sBuffer = \fread($fResource, $iBufferLen); - if (false !== $sBuffer) - { - echo $sBuffer; - static::ResetTimeLimit(); - continue; + if (false === $sBuffer) { + break; } - - break; + echo $sBuffer; + static::ResetTimeLimit(); } - - $bResult = true; } return $bResult; @@ -912,63 +609,44 @@ abstract class Utils /** * @param resource $rRead + * @param resource $rWrite */ - public static function MultipleStreamWriter($rRead, array $aWrite, int $iBufferLen = 8192, bool $bResetTimeLimit = true, bool $bFixCrLf = false, bool $bRewindOnComplete = false) : int + public static function WriteStream($rRead, $rWrite, int $iBufferLen = 8192, bool $bFixCrLf = false, bool $bRewindOnComplete = false) : int { - $mResult = false; - if (\is_resource($rRead) && \count($aWrite)) - { - $mResult = 0; - while (!\feof($rRead)) - { - $sBuffer = \fread($rRead, $iBufferLen); - if (false === $sBuffer) - { - $mResult = false; - break; - } - - if ('' === $sBuffer) - { - break; - } - - if ($bFixCrLf) - { - $sBuffer = \str_replace("\n", "\r\n", \str_replace("\r", '', $sBuffer)); - } - - $mResult += \strlen($sBuffer); - - foreach ($aWrite as $rWriteStream) - { - $mWriteResult = \fwrite($rWriteStream, $sBuffer); - if (false === $mWriteResult) - { - $mResult = false; - break 2; - } - } - - if ($bResetTimeLimit) - { - static::ResetTimeLimit(); - } - } + if (!\is_resource($rRead) || !\is_resource($rWrite)) { + return -1; } - if ($mResult && $bRewindOnComplete) - { - foreach ($aWrite as $rWriteStream) - { - if (\is_resource($rWriteStream)) - { - \rewind($rWriteStream); - } + $iResult = 0; + + while (!\feof($rRead)) { + $sBuffer = \fread($rRead, $iBufferLen); + if (false === $sBuffer) { + return -1; } + + if ('' === $sBuffer) { + break; + } + + if ($bFixCrLf) { + $sBuffer = \str_replace("\n", "\r\n", \str_replace("\r", '', $sBuffer)); + } + + $iResult += \strlen($sBuffer); + + if (false === \fwrite($rWrite, $sBuffer)) { + return -1; + } + + static::ResetTimeLimit(); } - return $mResult; + if ($bRewindOnComplete) { + \rewind($rWrite); + } + + return $iResult; } public static function Utf7ModifiedToUtf8(string $sStr) : string @@ -1016,8 +694,7 @@ abstract class Utils public static function ValidateDomain(string $sDomain, bool $bSimple = false) : bool { $aMatch = array(); - if ($bSimple) - { + if ($bSimple) { return \preg_match('/.+(\.[a-zA-Z]+)$/', $sDomain, $aMatch) && !empty($aMatch[1]); } @@ -1034,8 +711,7 @@ abstract class Utils public static function IdnToUtf8(string $sStr, bool $bLowerIfAscii = false) : string { - if (\strlen($sStr) && \preg_match('/(^|\.|@)xn--/i', $sStr)) - { + if (\strlen($sStr) && \preg_match('/(^|\.|@)xn--/i', $sStr)) { try { $sStr = \SnappyMail\IDN::anyToUtf8($sStr); @@ -1052,14 +728,12 @@ abstract class Utils $sUser = ''; $sDomain = $sStr; - if (false !== \strpos($sStr, '@')) - { + if (false !== \strpos($sStr, '@')) { $sUser = static::GetAccountNameFromEmail($sStr); $sDomain = static::GetDomainFromEmail($sStr); } - if (\strlen($sDomain) && \preg_match('/[^\x20-\x7E]/', $sDomain)) - { + if (\strlen($sDomain) && \preg_match('/[^\x20-\x7E]/', $sDomain)) { try { $sDomain = \SnappyMail\IDN::anyToAscii($sDomain); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Validator.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Validator.php deleted file mode 100644 index aed776de4..000000000 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Validator.php +++ /dev/null @@ -1,29 +0,0 @@ -= $iMin) && (null === $iMax || $iNumber <= $iMax); - } - - public static function PortInt(int $iPort) : bool - { - return static::RangeInt($iPort, 0, 65535); - } -} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php index 2db251a86..cea141a1c 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Xxtea.php @@ -22,10 +22,8 @@ class Xxtea { $aV = self::str2long($sString, true); $aK = self::str2long($sKey, false); - if (\count($aK) < 4) - { - for ($iIndex = \count($aK); $iIndex < 4; $iIndex++) - { + if (\count($aK) < 4) { + for ($iIndex = \count($aK); $iIndex < 4; $iIndex++) { $aK[$iIndex] = 0; } } @@ -36,12 +34,10 @@ class Xxtea $iDelta = 0x9E3779B9; $iQ = \floor(6 + 52 / ($iN + 1)); $iSum = 0; - while (0 < $iQ--) - { + while (0 < $iQ--) { $iSum = self::int32($iSum + $iDelta); $iE = $iSum >> 2 & 3; - for ($iPIndex = 0; $iPIndex < $iN; $iPIndex++) - { + for ($iPIndex = 0; $iPIndex < $iN; ++$iPIndex) { $iY = $aV[$iPIndex + 1]; $iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) + (($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ)); @@ -61,10 +57,8 @@ class Xxtea $aV = self::str2long($sEncryptedString, false); $aK = self::str2long($sKey, false); - if (\count($aK) < 4) - { - for ($iIndex = \count($aK); $iIndex < 4; $iIndex++) - { + if (\count($aK) < 4) { + for ($iIndex = \count($aK); $iIndex < 4; ++$iIndex) { $aK[$iIndex] = 0; } } @@ -76,11 +70,9 @@ class Xxtea $iDelta = 0x9E3779B9; $iQ = \floor(6 + 52 / ($iN + 1)); $iSum = self::int32($iQ * $iDelta); - while ($iSum != 0) - { + while ($iSum != 0) { $iE = $iSum >> 2 & 3; - for ($iPIndex = $iN; $iPIndex > 0; $iPIndex--) - { + for ($iPIndex = $iN; $iPIndex > 0; --$iPIndex) { $iZ = $aV[$iPIndex - 1]; $iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) + (($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ)); @@ -100,33 +92,25 @@ class Xxtea { $iLen = \count($aV); $iN = ($iLen - 1) << 2; - if ($aW) - { + if ($aW) { $iM = $aV[$iLen - 1]; - if (($iM < $iN - 3) || ($iM > $iN)) - { + if (($iM < $iN - 3) || ($iM > $iN)) { return false; } $iN = $iM; } $aS = array(); - for ($iIndex = 0; $iIndex < $iLen; $iIndex++) - { + for ($iIndex = 0; $iIndex < $iLen; ++$iIndex) { $aS[$iIndex] = \pack('V', $aV[$iIndex]); } - if ($aW) - { - return \substr(\join('', $aS), 0, $iN); - } - return \join('', $aS); + return $aW ? \substr(\join('', $aS), 0, $iN) : \join('', $aS); } private static function str2long(string $sS, string $sW) : array { $aV = \unpack('V*', $sS . \str_repeat("\0", (4 - \strlen($sS) % 4) & 3)); $aV = \array_values($aV); - if ($sW) - { + if ($sW) { $aV[\count($aV)] = \strlen($sS); } return $aV; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php index 6c9eed004..5144c1296 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/CacheClient.php @@ -22,10 +22,7 @@ class CacheClient */ private $oDriver; - /** - * @var string - */ - private $sCacheIndex = ''; + private string $sCacheIndex = ''; public function Set(string $sKey, string $sValue) : bool { @@ -56,13 +53,11 @@ class CacheClient { $sValue = ''; - if ($this->oDriver) - { + if ($this->oDriver) { $sValue = $this->oDriver->Get($sKey.$this->sCacheIndex); } - if ($bClearAfterGet) - { + if ($bClearAfterGet) { $this->Delete($sKey); } @@ -73,8 +68,7 @@ class CacheClient { $iTimer = 0; $sValue = $this->Get($sKey.'/TIMER'); - if (\strlen($sValue) && is_numeric($sValue)) - { + if (\strlen($sValue) && \is_numeric($sValue)) { $iTimer = (int) $sValue; } @@ -83,8 +77,7 @@ class CacheClient public function Delete(string $sKey) : self { - if ($this->oDriver) - { + if ($this->oDriver) { $this->oDriver->Delete($sKey.$this->sCacheIndex); } @@ -117,11 +110,9 @@ class CacheClient public function Verify(bool $bCache = false) : bool { - if ($this->oDriver) - { + if ($this->oDriver) { $sCacheData = \gmdate('Y-m-d-H'); - if ($bCache && $sCacheData === $this->Get('__verify_key__')) - { + if ($bCache && $sCacheData === $this->Get('__verify_key__')) { return true; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php index 8f1730edb..090aba857 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php @@ -18,19 +18,13 @@ namespace MailSo\Cache\Drivers; */ class APCU implements \MailSo\Cache\DriverInterface { - /** - * @var string - */ - private $sKeyPrefix; + private string $sKeyPrefix; function __construct(string $sKeyPrefix = '') { - $this->sKeyPrefix = $sKeyPrefix; - if (!empty($this->sKeyPrefix)) - { - $this->sKeyPrefix = - \preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($this->sKeyPrefix), '\\/')).'/'; - } + $this->sKeyPrefix = empty($sKeyPrefix) + ? $sKeyPrefix + : \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($sKeyPrefix), '\\/')).'/'; } public function Set(string $sKey, string $sValue) : bool @@ -51,12 +45,7 @@ class APCU implements \MailSo\Cache\DriverInterface public function GC(int $iTimeToClearInHours = 24) : bool { - if (0 === $iTimeToClearInHours) - { - return \apcu_clear_cache('user'); - } - - return false; + return (0 === $iTimeToClearInHours) ? \apcu_clear_cache('user') : false; } private function generateCachedKey(string $sKey) : string diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php index 0c32dc0e6..0fc9ab15e 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php @@ -18,30 +18,20 @@ namespace MailSo\Cache\Drivers; */ class File implements \MailSo\Cache\DriverInterface { - /** - * @var string - */ - private $sCacheFolder; + private string $sCacheFolder; - /** - * @var string - */ - private $sKeyPrefix; + private string $sKeyPrefix = ''; function __construct(string $sCacheFolder, string $sKeyPrefix = '') { - $this->sCacheFolder = $sCacheFolder; - $this->sCacheFolder = rtrim(trim($this->sCacheFolder), '\\/').'/'; - - $this->sKeyPrefix = $sKeyPrefix; - if (!empty($this->sKeyPrefix)) - { - $this->sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_', - rtrim(trim($this->sKeyPrefix), '\\/')), 5, '_'); + $this->sCacheFolder = \rtrim(\trim($sCacheFolder), '\\/').'/'; + if (!empty($sKeyPrefix)) { + $sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_', + \rtrim(\trim($sKeyPrefix), '\\/')), 5, '_'); $this->sKeyPrefix = '__/'. - \substr($this->sKeyPrefix, 0, 2).'/'.\substr($this->sKeyPrefix, 2, 2).'/'. - $this->sKeyPrefix.'/'; + \substr($sKeyPrefix, 0, 2).'/'.\substr($sKeyPrefix, 2, 2).'/'. + $sKeyPrefix.'/'; } } @@ -55,49 +45,41 @@ class File implements \MailSo\Cache\DriverInterface { $sValue = ''; $sPath = $this->generateCachedFileName($sKey); - if ('' !== $sPath && \file_exists($sPath)) - { + if ('' !== $sPath && \file_exists($sPath)) { $sValue = \file_get_contents($sPath); } - return \is_string($sValue) ? $sValue : ''; } public function Delete(string $sKey) : void { $sPath = $this->generateCachedFileName($sKey); - if ('' !== $sPath && \file_exists($sPath)) - { + if ('' !== $sPath && \file_exists($sPath)) { \unlink($sPath); } } public function GC(int $iTimeToClearInHours = 24) : bool { - if (0 === $iTimeToClearInHours) { - \MailSo\Base\Utils::RecRmDir($this->sCacheFolder); - } else { - \MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours); - } + \MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours); return true; } private function generateCachedFileName(string $sKey, bool $bMkDir = false) : string { $sFilePath = ''; - if (3 < \strlen($sKey)) - { + if (3 < \strlen($sKey)) { $sKeyPath = \sha1($sKey); - $sKeyPath = \substr($sKeyPath, 0, 2).'/'.\substr($sKeyPath, 2, 2).'/'.$sKeyPath; - - $sFilePath = $this->sCacheFolder.$this->sKeyPrefix.$sKeyPath; - $dir = \dirname($sFilePath); - if ($bMkDir && !\is_dir($dir) && !\mkdir($dir, 0700, true)) - { - $sFilePath = ''; + $sFilePath = $this->sCacheFolder . $this->sKeyPrefix + . \substr($sKeyPath, 0, 2) . '/' . \substr($sKeyPath, 2, 2) . '/' . $sKeyPath; + if ($bMkDir) { + $dir = \dirname($sFilePath); + if (!\is_dir($dir) && !\mkdir($dir, 0700, true)) { + \error_log("mkdir({$dir}) failed"); + $sFilePath = ''; + } } } - return $sFilePath; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php index 746789c83..b5e2a5252 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php @@ -18,37 +18,27 @@ namespace MailSo\Cache\Drivers; */ class Memcache implements \MailSo\Cache\DriverInterface { - /** - * @var int - */ - private $iExpire; + private int $iExpire; /** - * @var \Memcache|null + * @var \Memcache|\Memcached|null */ private $oMem; - /** - * @var string - */ - private $sKeyPrefix; + private string $sKeyPrefix; function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200, string $sKeyPrefix = '') { $this->iExpire = 0 < $iExpire ? $iExpire : 43200; $this->oMem = \class_exists('Memcache',false) ? new \Memcache : new \Memcached; - if (!$this->oMem->addServer($sHost, \strpos($sHost, ':/') ? 0 : $this->iPort)) - { + if (!$this->oMem->addServer($sHost, \strpos($sHost, ':/') ? 0 : $iPort)) { $this->oMem = null; } - $this->sKeyPrefix = $sKeyPrefix; - if (!empty($this->sKeyPrefix)) - { - $this->sKeyPrefix = - \preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($this->sKeyPrefix), '\\/')).'/'; - } + $this->sKeyPrefix = empty($sKeyPrefix) + ? $sKeyPrefix + : \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($this->sKeyPrefix), '\\/')) . '/'; } public function Set(string $sKey, string $sValue) : bool @@ -64,19 +54,14 @@ class Memcache implements \MailSo\Cache\DriverInterface public function Delete(string $sKey) : void { - if ($this->oMem) - { - $this->oMem->delete($this->generateCachedKey($sKey)); - } + $this->oMem && $this->oMem->delete($this->generateCachedKey($sKey)); } public function GC(int $iTimeToClearInHours = 24) : bool { - if (0 === $iTimeToClearInHours && $this->oMem) - { + if (0 === $iTimeToClearInHours && $this->oMem) { return $this->oMem->flush(); } - return false; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php index 94345c037..9271f0cb0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php @@ -18,21 +18,14 @@ namespace MailSo\Cache\Drivers; */ class Redis implements \MailSo\Cache\DriverInterface { - /** - * @var int - */ - private $iExpire; + private int $iExpire; /** - * @var \Memcache|null + * @var \Predis\Client|null */ private $oRedis; - /** - * @var string - */ - private $sKeyPrefix; - + private string $sKeyPrefix; function __construct(string $sHost = '127.0.0.1', int $iPort = 6379, int $iExpire = 43200, string $sKeyPrefix = '') { @@ -49,8 +42,7 @@ class Redis implements \MailSo\Cache\DriverInterface $this->oRedis->connect(); - if (!$this->oRedis->isConnected()) - { + if (!$this->oRedis->isConnected()) { $this->oRedis = null; } } @@ -60,18 +52,14 @@ class Redis implements \MailSo\Cache\DriverInterface unset($oExc); } - $this->sKeyPrefix = $sKeyPrefix; - if (!empty($this->sKeyPrefix)) - { - $this->sKeyPrefix = - \preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($this->sKeyPrefix), '\\/')).'/'; - } + $this->sKeyPrefix = empty($sKeyPrefix) + ? $sKeyPrefix + : \preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($sKeyPrefix), '\\/')) . '/'; } public function Set(string $sKey, string $sValue) : bool { - if (!$this->oRedis) - { + if (!$this->oRedis) { return false; } @@ -88,16 +76,14 @@ class Redis implements \MailSo\Cache\DriverInterface public function Delete(string $sKey) : void { - if ($this->oRedis) - { + if ($this->oRedis) { $this->oRedis->del($this->generateCachedKey($sKey)); } } public function GC(int $iTimeToClearInHours = 24) : bool { - if (0 === $iTimeToClearInHours && $this->oRedis) - { + if (0 === $iTimeToClearInHours && $this->oRedis) { return $this->oRedis->flushdb(); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Config.php b/snappymail/v/0.0.0/app/libraries/MailSo/Config.php index 8d6fe7dc3..c09480bb0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Config.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Config.php @@ -16,53 +16,5 @@ namespace MailSo; */ class Config { - /** - * @var int - */ - public static $MessageListFastSimpleSearch = true; - - /** - * @var int - */ - public static $MessageListCountLimitTrigger = 0; - - /** - * @var int - */ - public static $MessageListDateFilter = 0; - - /** - * @var string - */ - public static $MessageListPermanentFilter = ''; - - /** - * @var int - */ - public static $LargeThreadLimit = 50; - - /** - * @var bool - */ - public static $MessageAllHeaders = false; - - /** - * @var bool - */ - public static $LogSimpleLiterals = false; - - /** - * @var bool - */ - public static $CheckNewMessages = true; - - /** - * @var string - */ - public static $BoundaryPrefix = '_Part_'; - - /** - * @var int - */ - public static $ImapTimeout = 300; + public static string $BoundaryPrefix = '_Part_'; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Hooks.php b/snappymail/v/0.0.0/app/libraries/MailSo/Hooks.php deleted file mode 100644 index bcf0b999c..000000000 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Hooks.php +++ /dev/null @@ -1,50 +0,0 @@ -sPartID; } - public function FileName() : string + public function FileName(bool $bCalculateOnEmpty = false) : string { - return $this->sFileName; + $sFileName = \trim($this->sFileName); + if (\strlen($sFileName) || !$bCalculateOnEmpty) { + return $sFileName; + } + + $sIdx = '-' . $this->PartID(); + + $sMimeType = $this->sContentType; + if ('message/rfc822' === $sMimeType) { + return "message{$sIdx}.eml"; + } + if ('text/calendar' === $sMimeType) { + return "calendar{$sIdx}.ics"; + } + if ('text/plain' === $sMimeType) { + return "part{$sIdx}.txt"; + } + if (\preg_match('@text/(vcard|html|csv|xml|css|asp)@', $sMimeType, $aMatch) + || \preg_match('@image/(png|jpeg|gif|bmp|cgm|ief|tiff|webp)@', $sMimeType, $aMatch)) { + return "part{$sIdx}.{$aMatch[1]}"; + } + if (\strlen($sMimeType)) { + return \str_replace('/', $sIdx.'.', $sMimeType); + } + + return ($this->isInline() ? 'inline' : 'part' ) . $sIdx; } public function ContentType() : string @@ -102,11 +93,6 @@ class BodyStructure return $this->sContentType; } - public function Size() : int - { - return $this->iSize; - } - public function EstimatedSize() : int { $fCoefficient = 1; @@ -128,52 +114,27 @@ class BodyStructure return $this->sCharset; } - public function ContentID() : string - { - return $this->sContentID; - } - - public function ContentLocation() : string - { - return $this->sLocation; - } - public function SubParts() : array { return $this->aSubParts; } - public function IsInline() : bool + public function isInline() : bool { return 'inline' === $this->sDisposition || \strlen($this->sContentID); } - public function IsImage() : bool + public function isText() : bool { - return 'image' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName); - } - - public function IsArchive() : bool - { - return 'archive' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName); - } - - public function IsPdf() : bool - { - return 'pdf' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName); - } - - public function IsDoc() : bool - { - return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName); + return 'text/html' === $this->sContentType || 'text/plain' === $this->sContentType; } public function IsPgpEncrypted() : bool { // https://datatracker.ietf.org/doc/html/rfc3156#section-4 return 'multipart/encrypted' === $this->sContentType - && !empty($this->aBodyParams['protocol']) - && 'application/pgp-encrypted' === \strtolower(\trim($this->aBodyParams['protocol'])) + && !empty($this->aContentTypeParams['protocol']) + && 'application/pgp-encrypted' === \strtolower(\trim($this->aContentTypeParams['protocol'])) // The multipart/encrypted body MUST consist of exactly two parts. && 2 === \count($this->aSubParts) && 'application/pgp-encrypted' === $this->aSubParts[0]->ContentType() @@ -185,8 +146,8 @@ class BodyStructure { // https://datatracker.ietf.org/doc/html/rfc3156#section-5 return 'multipart/signed' === $this->sContentType - && !empty($this->aBodyParams['protocol']) - && 'application/pgp-signature' === \strtolower(\trim($this->aBodyParams['protocol'])) + && !empty($this->aContentTypeParams['protocol']) + && 'application/pgp-signature' === \strtolower(\trim($this->aContentTypeParams['protocol'])) // The multipart/signed body MUST consist of exactly two parts. && 2 === \count($this->aSubParts) && $this->aSubParts[1]->IsPgpSignature(); @@ -197,22 +158,21 @@ class BodyStructure return \in_array($this->sContentType, ['application/pgp-signature', 'application/pkcs7-signature']); } - public function IsAttachBodyPart() : bool + public function IsAttachment() : bool { return 'application/pgp-encrypted' !== $this->sContentType && ( 'attachment' === $this->sDisposition || ( !\str_starts_with($this->sContentType, 'multipart/') - && 'text/html' !== $this->sContentType - && 'text/plain' !== $this->sContentType + && !$this->isText() ) ); } public function IsFlowedFormat() : bool { - return !empty($this->aBodyParams['format']) - && 'flowed' === \strtolower(\trim($this->aBodyParams['format'])) + return !empty($this->aContentTypeParams['format']) + && 'flowed' === \strtolower(\trim($this->aContentTypeParams['format'])) && !\in_array($this->sMailEncodingName, array('base64', 'quoted-printable')); } @@ -221,8 +181,7 @@ class BodyStructure $aParts = []; $gParts = $this->SearchByCallback(function ($oItem) { - return ('text/html' === $oItem->sContentType || 'text/plain' === $oItem->sContentType) - && !$oItem->IsAttachBodyPart(); + return $oItem->isText() && !$oItem->IsAttachment(); }); foreach ($gParts as $oPart) { $aParts[] = $oPart; @@ -235,7 +194,7 @@ class BodyStructure if (!$aParts) { $gEncryptedParts = $this->SearchByContentType('multipart/encrypted'); foreach ($gEncryptedParts as $oPart) { - if ($oPart->IsPgpEncrypted() && $oPart->SubParts()[1]->IsInline()) { + if ($oPart->IsPgpEncrypted() && $oPart->SubParts()[1]->isInline()) { return array($oPart->SubParts()[1]); } } @@ -247,25 +206,19 @@ class BodyStructure public function SearchCharset() : string { $gParts = $this->SearchByCallback(function ($oPart) { - return $oPart->Charset() - && ('text/html' === $oPart->sContentType || 'text/plain' === $oPart->sContentType) - && !$oPart->IsAttachBodyPart(); + return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment(); }); if (!$gParts->valid()) { $gParts = $this->SearchByCallback(function ($oPart) { - return $oPart->Charset() && $oPart->IsAttachBodyPart(); + return $oPart->Charset() && $oPart->IsAttachment(); }); } return $gParts->valid() ? $gParts->current()->Charset() : ''; } - /** - * @param mixed $fCallback - */ -// public function SearchByCallback($fCallback) : \Generator - public function SearchByCallback($fCallback, $parent = null) : iterable + public function SearchByCallback(callable $fCallback, /*BodyStructure*/ $parent = null) : iterable { if ($fCallback($this, $parent)) { yield $this; @@ -278,8 +231,8 @@ class BodyStructure public function SearchAttachmentsParts() : iterable { return $this->SearchByCallback(function ($oItem, $oParent) { -// return $oItem->IsAttachBodyPart(); - return $oItem->IsAttachBodyPart() && (!$oParent || !$oParent->IsPgpEncrypted()); +// return $oItem->IsAttachment(); + return $oItem->IsAttachment() && (!$oParent || !$oParent->IsPgpEncrypted()); }); } @@ -294,20 +247,15 @@ class BodyStructure public function GetPartByMimeIndex(string $sMimeIndex) : self { $oPart = null; - if (\strlen($sMimeIndex)) - { - if ($sMimeIndex === $this->sPartID) - { + if (\strlen($sMimeIndex)) { + if ($sMimeIndex === $this->sPartID) { $oPart = $this; } - if (null === $oPart) - { - foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) - { + if (null === $oPart) { + foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) { $oPart = $oSubPart->GetPartByMimeIndex($sMimeIndex); - if (null !== $oPart) - { + if (null !== $oPart) { break; } } @@ -320,60 +268,43 @@ class BodyStructure private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset) : string { $sResult = ''; - if (isset($aParams[$sParamName])) - { + if (isset($aParams[$sParamName])) { $sResult = \MailSo\Base\Utils::DecodeHeaderValue($aParams[$sParamName], $sCharset); - } - else if (isset($aParams[$sParamName.'*'])) - { + } else if (isset($aParams[$sParamName.'*'])) { $aValueParts = \explode("''", $aParams[$sParamName.'*'], 2); - if (2 === \count($aValueParts)) - { + if (2 === \count($aValueParts)) { $sCharset = isset($aValueParts[0]) ? $aValueParts[0] : \MailSo\Base\Enumerations\Charset::UTF_8; - $sResult = \MailSo\Base\Utils::ConvertEncoding( \urldecode($aValueParts[1]), $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8); - } - else - { + } else { $sResult = \urldecode($aParams[$sParamName.'*']); } - } - else - { + } else { $sCharset = ''; $sCharsetIndex = -1; $aFileNames = array(); - foreach ($aParams as $sName => $sValue) - { + foreach ($aParams as $sName => $sValue) { $aMatches = array(); - if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches)) - { + if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches)) { $iIndex = (int) $aMatches[1]; - if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''")) - { + if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''")) { $aValueParts = \explode("''", $sValue, 2); - if (2 === \count($aValueParts) && \strlen($aValueParts[0])) - { + if (2 === \count($aValueParts) && \strlen($aValueParts[0])) { $sCharsetIndex = $iIndex; $sCharset = $aValueParts[0]; $sValue = $aValueParts[1]; } } - $aFileNames[$iIndex] = $sValue; } } - if (\count($aFileNames)) - { + if (\count($aFileNames)) { \ksort($aFileNames, SORT_NUMERIC); $sResult = \implode(\array_values($aFileNames)); $sResult = \urldecode($sResult); - - if (\strlen($sCharset)) - { + if (\strlen($sCharset)) { $sResult = \MailSo\Base\Utils::ConvertEncoding($sResult, $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8); } @@ -385,16 +316,15 @@ class BodyStructure public static function NewInstance(array $aBodyStructure, string $sPartID = '') : ?self { - if (2 > \count($aBodyStructure)) - { + if (2 > \count($aBodyStructure)) { return null; } $sContentTypeMain = ''; $sContentTypeSub = ''; $aSubParts = array(); - $aBodyParams = array(); - $sName = ''; + $aContentTypeParams = array(); + $sFileName = ''; $sCharset = ''; // \MailSo\Base\Enumerations\Charset::UTF_8 ? $sContentID = ''; $sDescription = ''; @@ -402,20 +332,16 @@ class BodyStructure $iSize = 0; $iExtraItemPos = 0; // list index of items which have no well-established position (such as 0, 1, 5, etc). - if (\is_array($aBodyStructure[0])) - { + if (\is_array($aBodyStructure[0])) { // Process multipart body structure $sContentTypeMain = 'multipart'; $sContentTypeSub = 'mixed'; // primary default $sSubPartIDPrefix = ''; - if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) - { + if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) { // This multi-part is root part of message. $sSubPartIDPrefix = $sPartID; $sPartID .= 'TEXT'; - } - else if (\strlen($sPartID)) - { + } else if (\strlen($sPartID)) { // This multi-part is a part of another multi-part. $sSubPartIDPrefix = $sPartID.'.'; } @@ -427,18 +353,15 @@ class BodyStructure ("text" "plain" ("charset" "utf-8") …) ("text" "html" …) */ - while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) - { + while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) { $oPart = self::NewInstance($aBodyStructure[$iExtraItemPos], $sSubPartIDPrefix.$iIndex); - if (!$oPart) - { + if (!$oPart) { return null; } // For multipart, we have no charset info in the part itself. Thus, // obtain charset from nested parts. - if (!$sCharset) - { + if (!$sCharset) { $sCharset = $oPart->Charset(); } @@ -451,26 +374,20 @@ class BodyStructure * Now process the subparts containter like: "alternative" ("boundary" "--boundary_id") … */ - if ($iExtraItemPos < \count($aBodyStructure)) - { - if (!\is_string($aBodyStructure[$iExtraItemPos])) - { + if ($iExtraItemPos < \count($aBodyStructure)) { + if (!\is_string($aBodyStructure[$iExtraItemPos])) { return null; } $sContentTypeSub = \strtolower($aBodyStructure[$iExtraItemPos]); ++$iExtraItemPos; - if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) - { - $aBodyParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]); + if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) { + $aContentTypeParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]); } } - } - else if (\is_string($aBodyStructure[0])) - { + } else if (\is_string($aBodyStructure[0])) { // Process simple (singlepart) body structure - if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1])) - { + if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1])) { return null; } @@ -478,39 +395,33 @@ class BodyStructure $sContentTypeSub = \strtolower($aBodyStructure[1]); $aBodyParamList = $aBodyStructure[2]; - if (\is_array($aBodyParamList)) - { - $aBodyParams = self::getKeyValueListFromArrayList($aBodyParamList); - if (isset($aBodyParams['charset'])) - { - $sCharset = $aBodyParams['charset']; + if (\is_array($aBodyParamList)) { + $aContentTypeParams = self::getKeyValueListFromArrayList($aBodyParamList); + if (isset($aContentTypeParams['charset'])) { + $sCharset = $aContentTypeParams['charset']; + } + $sFileName = self::decodeAttrParameter($aContentTypeParams, 'name', $sCharset); + if ($sFileName) { + $aContentTypeParams['name'] = $sFileName; } - - $sName = self::decodeAttrParameter($aBodyParams, 'name', $sCharset); } - if (null !== $aBodyStructure[3]) - { - if (!\is_string($aBodyStructure[3])) - { + if (null !== $aBodyStructure[3]) { + if (!\is_string($aBodyStructure[3])) { return null; } $sContentID = $aBodyStructure[3]; } - if (null !== $aBodyStructure[4]) - { - if (!\is_string($aBodyStructure[4])) - { + if (null !== $aBodyStructure[4]) { + if (!\is_string($aBodyStructure[4])) { return null; } $sDescription = $aBodyStructure[4]; } - if (null !== $aBodyStructure[5]) - { - if (!\is_string($aBodyStructure[5])) - { + if (null !== $aBodyStructure[5]) { + if (!\is_string($aBodyStructure[5])) { return null; } $sMailEncodingName = $aBodyStructure[5]; @@ -518,24 +429,20 @@ class BodyStructure $iSize = \is_numeric($aBodyStructure[6]) ? (int) $aBodyStructure[6] : -1; - if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) - { + if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) { // This is the only sub-part of the message (otherwise, it would be // one of sub-parts of a multi-part, and partID would already be fully set up). $sPartID .= '1'; } $iExtraItemPos = 7; - if ('text' === $sContentTypeMain) - { + if ('text' === $sContentTypeMain) { /** * A body type of type TEXT contains, immediately after the basic * fields, the size of the body in text lines. */ ++$iExtraItemPos; - } - else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub) - { + } else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub) { /** * A body type of type MESSAGE and subtype RFC822 contains, * immediately after the basic fields, the envelope structure, @@ -543,9 +450,7 @@ class BodyStructure */ $iExtraItemPos += 3; } - } - else - { + } else { return null; } @@ -553,20 +458,15 @@ class BodyStructure ++$iExtraItemPos; $sDisposition = ''; - $sFileName = ''; - if ($iExtraItemPos < \count($aBodyStructure)) - { + if ($iExtraItemPos < \count($aBodyStructure)) { $aDispList = $aBodyStructure[$iExtraItemPos]; - if (\is_array($aDispList) && 1 < \count($aDispList)) - { - if (!\is_string($aDispList[0])) - { + if (\is_array($aDispList) && 1 < \count($aDispList)) { + if (!\is_string($aDispList[0])) { return null; } $sDisposition = $aDispList[0]; - if (\is_array($aDispList[1])) - { + if (\is_array($aDispList[1])) { $aDispositionParams = self::getKeyValueListFromArrayList($aDispList[1]); $sFileName = self::decodeAttrParameter($aDispositionParams, 'filename', $sCharset); } @@ -575,32 +475,27 @@ class BodyStructure } $oStructure = new self; - $oStructure->sContentType = \strtolower($sContentTypeMain.'/'.$sContentTypeSub); + $oStructure->sContentType = \strtolower(\trim($sContentTypeMain.'/'.$sContentTypeSub)); + $oStructure->aContentTypeParams = $aContentTypeParams; $oStructure->sCharset = $sCharset; - $oStructure->aBodyParams = $aBodyParams; - $oStructure->sContentID = $sContentID; + $oStructure->sContentID = \trim($sContentID); $oStructure->sDescription = $sDescription; $oStructure->sMailEncodingName = \strtolower($sMailEncodingName); $oStructure->sDisposition = \strtolower($sDisposition); - $oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName ?: $sName); + $oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName); $oStructure->iSize = $iSize; $oStructure->sPartID = $sPartID; $oStructure->aSubParts = $aSubParts; - if ($iExtraItemPos < \count($aBodyStructure)) - { - if (\is_array($aBodyStructure[$iExtraItemPos])) - { + if ($iExtraItemPos < \count($aBodyStructure)) { + if (\is_array($aBodyStructure[$iExtraItemPos])) { $oStructure->sLanguage = \implode(',', $aBodyStructure[$iExtraItemPos]); - } - else if (\is_string($aBodyStructure[$iExtraItemPos])) - { + } else if (\is_string($aBodyStructure[$iExtraItemPos])) { $oStructure->sLanguage = $aBodyStructure[$iExtraItemPos]; } ++$iExtraItemPos; - if ($iExtraItemPos < \count($aBodyStructure) && \is_string($aBodyStructure[$iExtraItemPos])) - { + if ($iExtraItemPos < \count($aBodyStructure) && \is_string($aBodyStructure[$iExtraItemPos])) { $oStructure->sLocation = $aBodyStructure[$iExtraItemPos]; } } @@ -616,12 +511,9 @@ class BodyStructure { $aDict = array(); $iLen = \count($aList); - if (0 === ($iLen % 2)) - { - for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2) - { - if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1])) - { + if (0 === ($iLen % 2)) { + for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2) { + if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1])) { $aDict[\strtolower($aList[$iIndex])] = $aList[$iIndex + 1]; } } @@ -629,4 +521,19 @@ class BodyStructure return $aDict; } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + 'mimeIndex' => $this->sPartID, + 'mimeType' => $this->sContentType, +// 'mimeTypeParams' => $this->aContentTypeParams, + 'fileName' => \MailSo\Base\Utils::SecureFileName($this->FileName(true)), + 'estimatedSize' => $this->EstimatedSize(), + 'cId' => $this->sContentID, + 'contentLocation' => $this->sLocation, + 'isInline' => $this->isInline() + ); + } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php index 86c80b3ac..e3043f296 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/ACL.php @@ -24,7 +24,7 @@ trait ACL */ public function ACLAllow(string $sFolderName, string $command) : bool { - if ($this->IsSupported('ACL') || $this->IsSupported('RIGHTS=texk')) { + if ($this->hasCapability('ACL') || $this->CapabilityValue('RIGHTS')) { if ('MYRIGHTS' === $command) { return true; } @@ -128,7 +128,8 @@ trait ACL public function FolderMyRights(string $sFolderName) : ?ACLResponse { -// if ($this->IsSupported('ACL')) { +// if ($this->ACLAllow($sFolderName, 'MYRIGHTS')) { +// if ($this->hasCapability('ACL')) { $oResponses = $this->SendRequestGetResponse('MYRIGHTS', array($this->EscapeString($sFolderName))); foreach ($oResponses as $oResponse) { if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php index 9694cf556..a0017950b 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Folders.php @@ -13,10 +13,12 @@ namespace MailSo\Imap\Commands; use MailSo\Imap\Folder; +use MailSo\Imap\FolderCollection; use MailSo\Imap\FolderInformation; use MailSo\Imap\SequenceSet; use MailSo\Imap\Enumerations\FolderStatus; -use MailSo\Imap\Enumerations\FolderResponseStatus; +use MailSo\Imap\Enumerations\MessageFlag; +use MailSo\Imap\Enumerations\StoreAction; /** * @category MailSo @@ -24,19 +26,33 @@ use MailSo\Imap\Enumerations\FolderResponseStatus; */ trait Folders { + /** + * @throws \InvalidArgumentException + */ + public function FolderClear(string $sFolderFullName) : void + { + if (0 < $this->FolderSelect($sFolderFullName)->MESSAGES) { + $this->MessageStoreFlag(new SequenceSet('1:*', false), + array(MessageFlag::DELETED), + StoreAction::ADD_FLAGS_SILENT + ); + $this->FolderExpunge(); + } + } + /** * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderCreate(string $sFolderName) : self + public function FolderCreate(string $sFolderName, bool $bSubscribe = false) : void { $this->SendRequestGetResponse('CREATE', array( $this->EscapeFolderName($sFolderName) // , ['(USE (\Drafts \Sent))'] RFC 6154 )); - return $this; + $bSubscribe && $this->FolderSubscribe($sFolderName); } /** @@ -45,23 +61,34 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderDelete(string $sFolderName) : self + public function FolderDelete(string $sFolderName) : void { + if (!$sFolderName || 'INBOX' === $sFolderName) { + throw new \InvalidArgumentException; + } + + $oInfo = $this->hasCapability('IMAP4rev2') + ? $this->FolderExamine($sFolderName) + : $this->FolderStatus($sFolderName); + if ($oInfo->MESSAGES) { + throw new \MailSo\Mail\Exceptions\NonEmptyFolder; + } + + $this->FolderUnsubscribe($sFolderName); + $this->FolderUnselect(); + // Uncomment will work issue #124 ? // $this->selectOrExamineFolder($sFolderName, true); - $this->SendRequestGetResponse('DELETE', - array($this->EscapeFolderName($sFolderName))); + $this->SendRequestGetResponse('DELETE', [$this->EscapeFolderName($sFolderName)]); // $this->FolderCheck(); -// $this->FolderUnselect(); // Will this workaround solve Dovecot issue #124 ? try { $this->FolderRename($sFolderName, "{$sFolderName}-dummy"); $this->FolderRename("{$sFolderName}-dummy", $sFolderName); - } catch (\Throwable $e) { + } catch (\Throwable $oException) { + $this->writeLogException($oException, \LOG_WARNING, false); } - - return $this; } /** @@ -70,11 +97,9 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderSubscribe(string $sFolderName) : self + public function FolderSubscribe(string $sFolderName) : void { - $this->SendRequestGetResponse('SUBSCRIBE', - array($this->EscapeFolderName($sFolderName))); - return $this; + $this->SendRequestGetResponse('SUBSCRIBE', [$this->EscapeFolderName($sFolderName)]); } /** @@ -83,11 +108,9 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderUnsubscribe(string $sFolderName) : self + public function FolderUnsubscribe(string $sFolderName) : void { - $this->SendRequestGetResponse('UNSUBSCRIBE', - array($this->EscapeFolderName($sFolderName))); - return $this; + $this->SendRequestGetResponse('UNSUBSCRIBE', [$this->EscapeFolderName($sFolderName)]); } /** @@ -96,12 +119,44 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderRename(string $sOldFolderName, string $sNewFolderName) : self + public function FolderRename(string $sOldFolderName, string $sNewFolderName) : void { - $this->SendRequestGetResponse('RENAME', array( + $this->SendRequestGetResponse('RENAME', [ $this->EscapeFolderName($sOldFolderName), - $this->EscapeFolderName($sNewFolderName))); - return $this; + $this->EscapeFolderName($sNewFolderName) + ]); + } + + private function FolderStatusItems() : array + { + $aStatusItems = array( + FolderStatus::MESSAGES, + FolderStatus::UNSEEN, + FolderStatus::UIDNEXT, + FolderStatus::UIDVALIDITY + ); + // RFC 4551 + if ($this->hasCapability('CONDSTORE')) { + $aStatusItems[] = FolderStatus::HIGHESTMODSEQ; + } + // RFC 7889 + if ($this->hasCapability('APPENDLIMIT')) { + $aStatusItems[] = FolderStatus::APPENDLIMIT; + } + // RFC 8474 + if ($this->hasCapability('OBJECTID')) { + $aStatusItems[] = FolderStatus::MAILBOXID; +/* + } else if ($this->hasCapability('X-DOVECOT')) { + $aStatusItems[] = 'X-GUID'; +*/ + } +/* // STATUS SIZE can take a significant amount of time, therefore not active + if ($this->hasCapability('IMAP4rev2')) { + $aStatusItems[] = FolderStatus::SIZE; + } +*/ + return $aStatusItems; } /** @@ -112,48 +167,26 @@ trait Folders * * https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.11 */ - public function FolderStatus(string $sFolderName) : FolderInformation + public function FolderStatus(string $sFolderName, bool $bSelect = false) : FolderInformation { - $aStatusItems = array( - FolderResponseStatus::MESSAGES, - FolderResponseStatus::UNSEEN, - FolderResponseStatus::UIDNEXT, - FolderResponseStatus::UIDVALIDITY - ); - if ($this->IsSupported('CONDSTORE')) { - $aStatusItems[] = FolderResponseStatus::HIGHESTMODSEQ; - } - if ($this->IsSupported('APPENDLIMIT')) { - $aStatusItems[] = FolderResponseStatus::APPENDLIMIT; - } - if ($this->IsSupported('OBJECTID')) { - $aStatusItems[] = FolderResponseStatus::MAILBOXID; -/* - } else if ($this->IsSupported('X-DOVECOT')) { - $aStatusItems[] = 'X-GUID'; -*/ - } -/* // STATUS SIZE can take a significant amount of time, therefore not active - if ($this->IsSupported('IMAP4rev2')) { - $aStatusItems[] = FolderResponseStatus::SIZE; - } -*/ $oFolderInfo = $this->oCurrentFolderInfo; $bReselect = false; $bWritable = false; - if ($oFolderInfo && $sFolderName === $oFolderInfo->FolderName) { + if ($oFolderInfo && $sFolderName === $oFolderInfo->FullName) { + if ($oFolderInfo->hasStatus) { + return $oFolderInfo; + } + /** * There's a long standing IMAP CLIENTBUG where STATUS command is executed * after SELECT/EXAMINE on same folder (it should not). * So we must unselect the folder to be able to get the APPENDLIMIT and UNSEEN. */ /* - if ($this->IsSupported('ESEARCH')) { - $aResult = $oFolderInfo->getStatusItems(); - // SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message - $aResult['UNSEEN'] = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT']; - return $aResult; + if ($this->hasCapability('ESEARCH') && !isset($oFolderInfo->UNSEEN)) { + $oFolderInfo->UNSEEN = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT']; } + return $oFolderInfo; */ $bWritable = $oFolderInfo->IsWritable; $bReselect = true; @@ -161,30 +194,49 @@ trait Folders } $oInfo = new FolderInformation($sFolderName, false); - $this->SendRequest('STATUS', array($this->EscapeFolderName($sFolderName), $aStatusItems)); + $this->SendRequest('STATUS', array($this->EscapeFolderName($sFolderName), $this->FolderStatusItems())); foreach ($this->yieldUntaggedResponses() as $oResponse) { $oInfo->setStatusFromResponse($oResponse); } - if ($bReselect) { - $this->selectOrExamineFolder($sFolderName, $bWritable, false); -// $this->oCurrentFolderInfo->UNSEEN = $oInfo->UNSEEN; + if ($bReselect || $bSelect) { + // Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot + $oFolderInfo = $this->selectOrExamineFolder($sFolderName, $bSelect || $bWritable, false); + $oFolderInfo->MESSAGES = \max(0, $oFolderInfo->MESSAGES, $oInfo->MESSAGES); + // SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message. + // And deprecated in IMAP4rev2, so we set it to the amount of unseen messages + $oFolderInfo->UNSEEN = \max(0, $oInfo->UNSEEN); + $oFolderInfo->UIDNEXT = \max(0, $oFolderInfo->UIDNEXT, $oInfo->UIDNEXT); + $oFolderInfo->UIDVALIDITY = \max(0, $oFolderInfo->UIDVALIDITY, $oInfo->UIDVALIDITY); + $oFolderInfo->HIGHESTMODSEQ = \max(0, $oInfo->HIGHESTMODSEQ); + $oFolderInfo->APPENDLIMIT = \max(0, $oFolderInfo->APPENDLIMIT, $oInfo->APPENDLIMIT); + $oFolderInfo->MAILBOXID = $oFolderInfo->MAILBOXID ?: $oInfo->MAILBOXID; +// $oFolderInfo->SIZE = \max($oFolderInfo->SIZE, $oInfo->SIZE); +// $oFolderInfo->RECENT = \max(0, $oFolderInfo->RECENT, $oInfo->RECENT); + $oFolderInfo->hasStatus = $oInfo->hasStatus; + $oFolderInfo->generateETag($this); + return $oFolderInfo; } + $oInfo->generateETag($this); return $oInfo; } + public function FolderStatusAndSelect(string $sFolderName) : FolderInformation + { + return $this->FolderStatus($sFolderName, true); + } + /** * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderCheck() : self + public function FolderCheck() : void { if ($this->IsSelected()) { $this->SendRequestGetResponse('CHECK'); } - return $this; } /** @@ -209,25 +261,21 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderUnselect() : self + public function FolderUnselect() : void { if ($this->IsSelected()) { - if ($this->IsSupported('UNSELECT')) { + if ($this->hasCapability('UNSELECT')) { $this->SendRequestGetResponse('UNSELECT'); - $this->oCurrentFolderInfo = null; } else { try { $this->SendRequestGetResponse('SELECT', ['""']); // * OK [CLOSED] Previous mailbox closed. // 3 NO [CANNOT] Invalid mailbox name: Name is empty - } catch (\MailSo\Imap\Exceptions\NegativeResponseException $e) { - if ('NO' === $e->GetResponseStatus()) { - $this->oCurrentFolderInfo = null; - } + } catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException) { } } } - return $this; + $this->oCurrentFolderInfo = null; } /** @@ -243,7 +291,7 @@ trait Folders $sCmd = 'EXPUNGE'; $aArguments = array(); - if ($oUidRange && \count($oUidRange) && $oUidRange->UID && $this->IsSupported('UIDPLUS')) { + if ($oUidRange && \count($oUidRange) && $oUidRange->UID && $this->hasCapability('UIDPLUS')) { $sCmd = 'UID '.$sCmd; $aArguments = array((string) $oUidRange); } @@ -272,9 +320,9 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderSelect(string $sFolderName, bool $bReSelectSameFolders = false) : FolderInformation + public function FolderSelect(string $sFolderName, bool $bForceReselect = false) : FolderInformation { - return $this->selectOrExamineFolder($sFolderName, true, $bReSelectSameFolders); + return $this->selectOrExamineFolder($sFolderName, true, $bForceReselect); } /** @@ -288,9 +336,9 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderExamine(string $sFolderName, bool $bReSelectSameFolders = false) : FolderInformation + public function FolderExamine(string $sFolderName, bool $bForceReselect = false) : FolderInformation { - return $this->selectOrExamineFolder($sFolderName, $this->__FORCE_SELECT_ON_EXAMINE__, $bReSelectSameFolders); + return $this->selectOrExamineFolder($sFolderName, $this->Settings->force_select, $bForceReselect); } /** @@ -302,18 +350,17 @@ trait Folders * REQUIRED IMAP4rev2 untagged responses: FLAGS, EXISTS, LIST * REQUIRED IMAP4rev2 OK untagged responses: PERMANENTFLAGS, UIDNEXT, UIDVALIDITY */ - protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bReSelectSameFolders) : FolderInformation + protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bForceReselect) : FolderInformation { - if (!$bReSelectSameFolders + if (!$bForceReselect && $this->oCurrentFolderInfo - && $sFolderName === $this->oCurrentFolderInfo->FolderName - && $bIsWritable === $this->oCurrentFolderInfo->IsWritable + && $sFolderName === $this->oCurrentFolderInfo->FullName + && ($bIsWritable === $this->oCurrentFolderInfo->IsWritable || $this->oCurrentFolderInfo->IsWritable) ) { return $this->oCurrentFolderInfo; } - if (!\strlen(\trim($sFolderName))) - { + if (!\strlen(\trim($sFolderName))) { throw new \InvalidArgumentException; } @@ -321,7 +368,7 @@ trait Folders /* // RFC 5162 - if ($this->IsSupported('QRESYNC')) { + if ($this->hasCapability('QRESYNC')) { $this->Enable(['QRESYNC', 'CONDSTORE']); - the last known UIDVALIDITY, - the last known modification sequence, @@ -332,7 +379,7 @@ trait Folders } // RFC 4551 - if ($this->IsSupported('CONDSTORE')) { + if ($this->hasCapability('CONDSTORE')) { $aSelectParams[] = 'CONDSTORE'; } @@ -365,9 +412,9 @@ trait Folders $oResult->PermanentFlags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->OptionalResponse[1]); } } else if ('READ-ONLY' === $key) { -// $oResult->IsWritable = false; + $oResult->IsWritable = false; } else if ('READ-WRITE' === $key) { -// $oResult->IsWritable = true; + $oResult->IsWritable = true; } else if ('NOMODSEQ' === $key) { // https://datatracker.ietf.org/doc/html/rfc4551#section-3.1.2 } @@ -377,11 +424,20 @@ trait Folders else if (\count($oResponse->ResponseList) > 2 && 'FLAGS' === $oResponse->ResponseList[1] && \is_array($oResponse->ResponseList[2])) { - $oResult->Flags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->ResponseList[2]); + // These could be not permanent, so we don't use them +// $oResult->Flags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->ResponseList[2]); } } } + // SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message. + // IMAP4rev2 deprecated + $oResult->UNSEEN = null; +/* + if ($this->hasCapability('ESEARCH')) { + $oResult->UNSEEN = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT']; + } +*/ $this->oCurrentFolderInfo = $oResult; return $oResult; @@ -392,7 +448,7 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderList(string $sParentFolderName, string $sListPattern, bool $bIsSubscribeList = false, bool $bUseListStatus = false) : array + public function FolderList(string $sParentFolderName, string $sListPattern, bool $bIsSubscribeList = false, bool $bUseListStatus = false) : FolderCollection { $sCmd = 'LIST'; @@ -402,7 +458,7 @@ trait Folders if ($bIsSubscribeList) { // IMAP4rev2 deprecated $sCmd = 'LSUB'; - } else if ($this->IsSupported('LIST-EXTENDED')) { + } else if ($this->hasCapability('LIST-EXTENDED')) { // RFC 5258 $aReturnParams[] = 'SUBSCRIBED'; // $aReturnParams[] = 'CHILDREN'; @@ -412,7 +468,7 @@ trait Folders // $aParameters[0] = '()'; } // RFC 6154 - if ($this->IsSupported('SPECIAL-USE')) { + if ($this->hasCapability('SPECIAL-USE')) { $aReturnParams[] = 'SPECIAL-USE'; } } @@ -422,32 +478,9 @@ trait Folders // $aParameters[] = $this->EscapeString(\strlen(\trim($sListPattern)) ? $sListPattern : '*'); // RFC 5819 - if ($bUseListStatus && !$bIsSubscribeList && $this->IsSupported('LIST-STATUS')) - { - $aL = array( - FolderStatus::MESSAGES, - FolderStatus::UNSEEN, - FolderStatus::UIDNEXT - ); - // RFC 4551 - if ($this->IsSupported('CONDSTORE')) { - $aL[] = FolderStatus::HIGHESTMODSEQ; - } - // RFC 7889 - if ($this->IsSupported('APPENDLIMIT')) { - $aL[] = FolderStatus::APPENDLIMIT; - } - // RFC 8474 - if ($this->IsSupported('OBJECTID')) { - $aL[] = FolderStatus::MAILBOXID; -/* - } else if ($this->IsSupported('X-DOVECOT')) { - $aL[] = 'X-GUID'; -*/ - } - + if ($bUseListStatus && !$bIsSubscribeList && $this->hasCapability('LIST-STATUS')) { $aReturnParams[] = 'STATUS'; - $aReturnParams[] = $aL; + $aReturnParams[] = $this->FolderStatusItems(); } /* // RFC 5738 @@ -460,98 +493,94 @@ trait Folders $aParameters[] = $aReturnParams; } +/* $bPassthru = false; - $aReturn = array(); + if ($bPassthru) { + $this->SendRequest($sCmd, $aParameters); + $this->streamResponse(); + return []; + } +*/ // RFC 5464 - $bMetadata = !$bIsSubscribeList && $this->IsSupported('METADATA'); - $aMetadata = null; - if ($bMetadata) { - // Dovecot supports fetching all METADATA at once - $aMetadata = $this->getAllMetadata(); - } + $bMetadata = !$bIsSubscribeList && $this->hasCapability('METADATA'); + // Dovecot supports fetching all METADATA at once + $aMetadata = $bMetadata ? $this->getAllMetadata() : null; $this->SendRequest($sCmd, $aParameters); - if ($bPassthru) { - $this->streamResponse(); - } else { - $sDelimiter = ''; - $bInbox = false; - foreach ($this->yieldUntaggedResponses() as $oResponse) { - if ('STATUS' === $oResponse->StatusOrIndex && isset($oResponse->ResponseList[2])) { - $sFullName = $this->toUTF8($oResponse->ResponseList[2]); - if (!isset($aReturn[$sFullName])) { - $aReturn[$sFullName] = new Folder($sFullName); - } - $aReturn[$sFullName]->setStatusFromResponse($oResponse); - } - else if ($sCmd === $oResponse->StatusOrIndex && 5 === \count($oResponse->ResponseList)) { - try - { - $sFullName = $this->toUTF8($oResponse->ResponseList[4]); - - /** - * $oResponse->ResponseList[0] = * - * $oResponse->ResponseList[1] = LIST (all) | LSUB (subscribed) - * $oResponse->ResponseList[2] = Flags - * $oResponse->ResponseList[3] = Delimiter - * $oResponse->ResponseList[4] = FullName - */ - if (!isset($aReturn[$sFullName])) { - $oFolder = new Folder($sFullName, - $oResponse->ResponseList[3], $oResponse->ResponseList[2]); - $aReturn[$sFullName] = $oFolder; - } else { - $oFolder = $aReturn[$sFullName]; - $oFolder->setDelimiter($oResponse->ResponseList[3]); - $oFolder->setFlags($oResponse->ResponseList[2]); - } - - if ($oFolder->IsInbox()) { - $bInbox = true; - } - - if (!$sDelimiter) { - $sDelimiter = $oFolder->Delimiter(); - } - - if (isset($aMetadata[$oResponse->ResponseList[4]])) { - $oFolder->SetAllMetadata($aMetadata[$oResponse->ResponseList[4]]); - } - - $aReturn[$sFullName] = $oFolder; - } - catch (\InvalidArgumentException $oException) - { - $this->writeLogException($oException, \LOG_WARNING, false); - } - catch (\Throwable $oException) - { - $this->writeLogException($oException, \LOG_WARNING, false); - } + $aFolders = array(); + $oFolderCollection = new FolderCollection; + $sDelimiter = ''; + $bInbox = false; + foreach ($this->yieldUntaggedResponses() as $oResponse) { + if ('STATUS' === $oResponse->StatusOrIndex && isset($oResponse->ResponseList[2])) { + $sFullName = $this->toUTF8($oResponse->ResponseList[2]); + if (!isset($oFolderCollection[$sFullName])) { + $oFolderCollection[$sFullName] = new Folder($sFullName); } + $oFolderCollection[$sFullName]->setStatusFromResponse($oResponse); + $oFolderCollection[$sFullName]->generateETag($this); } + else if ($sCmd === $oResponse->StatusOrIndex && 5 === \count($oResponse->ResponseList)) { + try + { + $sFullName = $this->toUTF8($oResponse->ResponseList[4]); - if (!$bInbox && !$sParentFolderName && !isset($aReturn['INBOX'])) { - $aReturn['INBOX'] = new Folder('INBOX', $sDelimiter); + /** + * $oResponse->ResponseList[0] = * + * $oResponse->ResponseList[1] = LIST (all) | LSUB (subscribed) + * $oResponse->ResponseList[2] = Attribute flags + * $oResponse->ResponseList[3] = Delimiter + * $oResponse->ResponseList[4] = FullName + */ + if (isset($oFolderCollection[$sFullName])) { + $oFolder = $oFolderCollection[$sFullName]; + $oFolder->setDelimiter($oResponse->ResponseList[3]); + $oFolder->setAttributes($oResponse->ResponseList[2]); + } else { + $oFolder = new Folder($sFullName, $oResponse->ResponseList[3], $oResponse->ResponseList[2]); + $oFolderCollection[$sFullName] = $oFolder; + } + + $bInbox = $bInbox || $oFolder->IsInbox(); + + if (!$sDelimiter) { + $sDelimiter = $oFolder->Delimiter(); + } + + if (isset($aMetadata[$oResponse->ResponseList[4]])) { + $oFolder->SetAllMetadata($aMetadata[$oResponse->ResponseList[4]]); + } + } + catch (\Throwable $oException) + { + $this->writeLogException($oException, \LOG_WARNING, false); + } } } +// $iOptimizationLimit = $this->Settings->folder_list_limit; +// $oFolderCollection->Optimized = 10 < $iOptimizationLimit && $oFolderCollection->count() > $iOptimizationLimit; + // RFC 5464 - if ($bMetadata && !$aMetadata /*&& 50 < \count($aReturn)*/) { - foreach ($aReturn as $oFolder) { -// if (2 > \substr_count($oFolder->FullName(), $oFolder->Delimiter())) + if ($bMetadata && !$aMetadata /*&& 50 < $oFolderCollection->count()*/) { + foreach ($oFolderCollection as $oFolder) { +// if (2 > \substr_count($oFolder->FullName, $oFolder->Delimiter())) try { $oFolder->SetAllMetadata( - $this->getMetadata($oFolder->FullName(), ['/shared', '/private'], ['DEPTH'=>'infinity']) + $this->getMetadata($oFolder->FullName, ['/shared', '/private'], ['DEPTH'=>'infinity']) ); - } catch (\Throwable $e) { + } catch (\Throwable $oException) { // Ignore error } } } - return $aReturn; + if (!$bInbox && !$sParentFolderName && !isset($oFolderCollection['INBOX'])) { + $oFolderCollection['INBOX'] = new Folder('INBOX', $sDelimiter); + } + + return $oFolderCollection; } /** @@ -559,7 +588,7 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderSubscribeList(string $sParentFolderName, string $sListPattern) : array + public function FolderSubscribeList(string $sParentFolderName, string $sListPattern) : FolderCollection { return $this->FolderList($sParentFolderName, $sListPattern, true); } @@ -569,7 +598,7 @@ trait Folders * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function FolderStatusList(string $sParentFolderName, string $sListPattern) : array + public function FolderStatusList(string $sParentFolderName, string $sListPattern) : FolderCollection { return $this->FolderList($sParentFolderName, $sListPattern, false, true); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php index ab3af2be8..d83fb5ff4 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Messages.php @@ -16,7 +16,9 @@ use MailSo\Imap\FetchResponse; use MailSo\Imap\Enumerations\FetchType; use MailSo\Imap\ResponseCollection; use MailSo\Imap\SequenceSet; +use MailSo\Imap\Enumerations\MessageFlag; use MailSo\Imap\Enumerations\ResponseType; +use MailSo\Imap\Enumerations\StoreAction; /** * @category MailSo @@ -30,11 +32,10 @@ trait Messages * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function Fetch(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : array + public function FetchIterate(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : iterable { - if (!\strlen(\trim($sIndexRange))) - { - $this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true); + if (!\strlen(\trim($sIndexRange))) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } $aReturn = array(); @@ -44,8 +45,7 @@ trait Messages FetchType::UID, FetchType::RFC822_SIZE ); - foreach ($aInputFetchItems as $mFetchKey) - { + foreach ($aInputFetchItems as $mFetchKey) { switch ($mFetchKey) { case FetchType::UID: @@ -77,20 +77,20 @@ trait Messages break; } } - if ($this->IsSupported('OBJECTID')) { + if ($this->hasCapability('OBJECTID')) { $aFetchItems[] = FetchType::EMAILID; $aFetchItems[] = FetchType::THREADID; - } else if ($this->IsSupported('X-GM-EXT-1')) { + } else if ($this->hasCapability('X-GM-EXT-1')) { // https://developers.google.com/gmail/imap/imap-extensions $aFetchItems[] = 'X-GM-MSGID'; $aFetchItems[] = 'X-GM-THRID'; /* - } else if ($this->IsSupported('X-DOVECOT')) { + } else if ($this->hasCapability('X-DOVECOT')) { $aFetchItems[] = 'X-GUID'; */ } /* - if ($this->IsSupported('X-GM-EXT-1') && \in_array(FetchType::FLAGS, $aFetchItems)) { + if ($this->hasCapability('X-GM-EXT-1') && \in_array(FetchType::FLAGS, $aFetchItems)) { $aFetchItems[] = 'X-GM-LABELS'; } */ @@ -112,31 +112,42 @@ trait Messages foreach ($this->yieldUntaggedResponses() as $oResponse) { if (FetchResponse::isValidImapResponse($oResponse)) { if (FetchResponse::hasUidAndSize($oResponse)) { - $aReturn[] = new FetchResponse($oResponse); - } else if ($this->oLogger) { - $this->oLogger->Write('Skipped Imap Response! ['.$oResponse.']', \LOG_NOTICE); + yield new FetchResponse($oResponse); + } else { + $this->logWrite('Skipped Imap Response! ['.$oResponse.']', \LOG_NOTICE); } } } } finally { $this->aFetchCallbacks = array(); } + } + public function Fetch(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : array + { + $aReturn = array(); + foreach ($this->FetchIterate($aInputFetchItems, $sIndexRange, $bIndexIsUid) as $oFetchResponse) { + $aReturn[] = $oFetchResponse; + } return $aReturn; } /** * Appends message to specified folder * - * @param resource $rMessageAppendStream + * @param resource $rMessageStream * * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function MessageAppendStream(string $sFolderName, $rMessageAppendStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int + public function MessageAppendStream(string $sFolderName, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int { + if (!\is_resource($rMessageStream) || !\strlen($sFolderName) || 1 > $iStreamSize) { + throw new \InvalidArgumentException; + } + $aParams = array( $this->EscapeFolderName($sFolderName), $aFlagsList @@ -147,7 +158,7 @@ trait Messages /* // RFC 3516 || RFC 6855 section-4 - if ($this->IsSupported('BINARY') || $this->IsSupported('UTF8=ACCEPT')) { + if ($this->hasCapability('BINARY') || $this->hasCapability('UTF8=ACCEPT')) { $aParams[] = '~{'.$iStreamSize.'}'; } */ @@ -155,14 +166,14 @@ trait Messages $this->SendRequestGetResponse('APPEND', $aParams); - return $this->writeMessageStream($rMessageAppendStream); + return $this->writeMessageStream($rMessageStream); } private function writeMessageStream($rMessageStream) : ?int { $this->writeLog('Write to connection stream', \LOG_INFO); - \MailSo\Base\Utils::MultipleStreamWriter($rMessageStream, array($this->ConnectionResource())); + \MailSo\Base\Utils::WriteStream($rMessageStream, $this->ConnectionResource()); $this->sendRaw(''); $oResponses = $this->getResponse(); @@ -196,12 +207,14 @@ trait Messages * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function MessageCopy(string $sToFolder, SequenceSet $oRange) : ResponseCollection + public function MessageCopy(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : ResponseCollection { - if (!\count($oRange)) { - $this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true); + if (!$sFromFolder || !$sToFolder || !\count($oRange)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } + $this->FolderSelect($sFromFolder); + return $this->SendRequestGetResponse( $oRange->UID ? 'UID COPY' : 'COPY', array((string) $oRange, $this->EscapeFolderName($sToFolder)) @@ -214,22 +227,48 @@ trait Messages * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function MessageMove(string $sToFolder, SequenceSet $oRange) : ResponseCollection + public function MessageMove(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : void { - if (!\count($oRange)) { - $this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true); + if (!$sFromFolder || !$sToFolder || !\count($oRange)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } - if (!$this->IsSupported('MOVE')) { - $this->writeLogException( - new \MailSo\RuntimeException('Move is not supported'), - \LOG_ERR, true); + if ($this->hasCapability('MOVE')) { + $this->FolderSelect($sFromFolder); + $this->SendRequestGetResponse( + $oRange->UID ? 'UID MOVE' : 'MOVE', + array((string) $oRange, $this->EscapeFolderName($sToFolder)) + ); + } else { + $this->MessageCopy($sFromFolder, $sToFolder, $oRange); + $this->MessageDelete($sFromFolder, $oRange, true); + } + } + + /** + * @throws \InvalidArgumentException + * @throws \MailSo\RuntimeException + * @throws \MailSo\Net\Exceptions\* + * @throws \MailSo\Imap\Exceptions\* + */ + public function MessageDelete(string $sFolder, SequenceSet $oRange, bool $bExpungeAll = false) : void + { + if (!$sFolder || !\count($oRange)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } - return $this->SendRequestGetResponse( - $oRange->UID ? 'UID MOVE' : 'MOVE', - array((string) $oRange, $this->EscapeFolderName($sToFolder)) + $this->FolderSelect($sFolder); + + $this->MessageStoreFlag($oRange, + array(MessageFlag::DELETED), + StoreAction::ADD_FLAGS_SILENT ); + + if ($bExpungeAll && $this->Settings->expunge_all_on_delete) { + $this->FolderExpunge(); + } else { + $this->FolderExpunge($oRange); + } } /** @@ -246,14 +285,14 @@ trait Messages */ public function MessageReplaceStream(string $sFolderName, int $iUid, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int { - if (1 > $iUid || !$this->IsSupported('REPLACE')) { + if (1 > $iUid || !$this->hasCapability('REPLACE')) { $this->FolderSelect($sFolderName); $iNewUid = $this->MessageAppendStream($sFolderName, $rMessageStream, $iStreamSize, $aFlagsList, $iDateTime); if ($iUid) { $oRange = new SequenceSet([$iUid]); $this->MessageStoreFlag($oRange, - array(\MailSo\Imap\Enumerations\MessageFlag::DELETED), - \MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT + array(MessageFlag::DELETED), + StoreAction::ADD_FLAGS_SILENT ); $this->FolderExpunge($oRange); } @@ -271,7 +310,7 @@ trait Messages /* // RFC 3516 || RFC 6855 section-4 - if ($this->IsSupported('BINARY') || $this->IsSupported('UTF8=ACCEPT')) { + if ($this->hasCapability('BINARY') || $this->hasCapability('UTF8=ACCEPT')) { $aParams[] = '~{'.$iStreamSize.'}'; } */ @@ -287,6 +326,7 @@ trait Messages * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* + * $sStoreAction = \MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT */ public function MessageStoreFlag(SequenceSet $oRange, array $aInputStoreItems, string $sStoreAction) : ?ResponseCollection { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php index f0246a18d..a01a4fb12 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Metadata.php @@ -96,23 +96,23 @@ trait Metadata public function ServerGetMetadata(array $aEntries, array $aOptions = []) : array { - return $this->IsSupported('METADATA-SERVER') + return $this->hasCapability('METADATA-SERVER') ? $this->getMetadata('', $aEntries, $aOptions) : []; } public function FolderGetMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array { - return $this->IsSupported('METADATA') + return $this->hasCapability('METADATA') ? $this->getMetadata($sFolderName, $aEntries, $aOptions) : []; } public function FolderSetMetadata(string $sFolderName, array $aEntries) : void { - if ($this->IsSupported('METADATA')) { + if ($this->hasCapability('METADATA')) { if (!$aEntries) { - throw new \MailSo\Base\Exceptions\InvalidArgumentException("Wrong argument for SETMETADATA command"); + throw new \InvalidArgumentException('Wrong argument for SETMETADATA command'); } $arguments = [$this->EscapeFolderName($sFolderName)]; @@ -126,4 +126,8 @@ trait Metadata } } + public function FolderRemoveMetadata($sFolderName, array $aEntries) : void + { + $this->FolderSetMetadata($sFolderName, \array_fill_keys(\array_keys($aEntries), null)); + } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php index ed8a88861..480be15cb 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Commands/Quota.php @@ -47,7 +47,7 @@ trait Quota // * QUOTA "User quota" (STORAGE 1284645 2097152)\r\n private function getQuota(bool $root, string $sFolderName) : ?array { - if (!$this->IsSupported('QUOTA')) { + if (!$this->hasCapability('QUOTA')) { return null; } $oResponseCollection = $this->SendRequest(($root?'GETQUOTAROOT':'GETQUOTA') . " {$this->EscapeFolderName($sFolderName)}"); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FolderResponseStatus.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FolderResponseStatus.php deleted file mode 100644 index eaabe002e..000000000 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Enumerations/FolderResponseStatus.php +++ /dev/null @@ -1,35 +0,0 @@ -oImapResponse = $oImapResponse; - $this->aEnvelopeCache = null; } public function GetEnvelope(bool $bForce = false) : ?array { - if (null === $this->aEnvelopeCache || $bForce) - { + if (null === $this->aEnvelopeCache || $bForce) { $this->aEnvelopeCache = $this->GetFetchValue(Enumerations\FetchType::ENVELOPE); } return $this->aEnvelopeCache; @@ -54,13 +46,10 @@ class FetchResponse { $oResult = null; $aEmails = $this->GetFetchEnvelopeValue($iIndex); - if (\is_array($aEmails) && \count($aEmails)) - { + if (\is_array($aEmails) && \count($aEmails)) { $oResult = new \MailSo\Mime\EmailCollection; - foreach ($aEmails as $aEmailItem) - { - if (\is_array($aEmailItem) && 4 === \count($aEmailItem)) - { + foreach ($aEmails as $aEmailItem) { + if (\is_array($aEmailItem) && 4 === \count($aEmailItem)) { $sDisplayName = \MailSo\Base\Utils::DecodeHeaderValue( self::findEnvelopeIndex($aEmailItem, 0, ''), $sParentCharset); @@ -70,8 +59,7 @@ class FetchResponse $sLocalPart = self::findEnvelopeIndex($aEmailItem, 2, ''); $sDomainPart = self::findEnvelopeIndex($aEmailItem, 3, ''); - if (\strlen($sLocalPart) && \strlen($sDomainPart)) - { + if (\strlen($sLocalPart) && \strlen($sDomainPart)) { $oResult->append( new \MailSo\Mime\Email($sLocalPart.'@'.$sDomainPart, $sDisplayName) ); @@ -122,18 +110,15 @@ class FetchResponse { $bNextIsValue = false; - if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) - { - foreach ($this->oImapResponse->ResponseList[3] as $mItem) - { - if ($bNextIsValue) - { + if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) { + foreach ($this->oImapResponse->ResponseList[3] as $mItem) { + if ($bNextIsValue) { return (string) $mItem; } if (\is_string($mItem) && ( $mItem === 'BODY[HEADER]' || - 0 === \strpos($mItem, 'BODY[HEADER.FIELDS') || + \str_starts_with($mItem, 'BODY[HEADER.FIELDS') || $mItem === 'BODY[MIME]')) { $bNextIsValue = true; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php index d9c10d781..e53fb887d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Folder.php @@ -11,58 +11,54 @@ namespace MailSo\Imap; +use MailSo\Imap\Enumerations\MetadataKeys; + /** * @category MailSo * @package Imap */ -class Folder +class Folder implements \JsonSerializable { // RFC5258 Response data STATUS items when using LIST-EXTENDED use Traits\Status; - /** - * @var string - */ - private $sFullName; + private ?string $sDelimiter; - /** - * @var string - */ - private $sDelimiter; - - /** - * @var array - */ - private $aFlagsLowerCase; + private array $aAttributes; /** * RFC 5464 */ - private $aMetadata = array(); + private array $aMetadata = array(); /** * @throws \InvalidArgumentException */ - function __construct(string $sFullName, string $sDelimiter = null, array $aFlags = array()) + function __construct(string $sFullName, string $sDelimiter = null, array $aAttributes = array()) { if (!\strlen($sFullName)) { throw new \InvalidArgumentException; } - $this->sFullName = $sFullName; + $this->FullName = $sFullName; $this->setDelimiter($sDelimiter); - $this->setFlags($aFlags); + $this->setAttributes($aAttributes); /* // RFC 5738 - if (\in_array('\\noutf8', $this->aFlagsLowerCase)) { + if (\in_array('\\noutf8', $this->aAttributes)) { } - if (\in_array('\\utf8only', $this->aFlagsLowerCase)) { + if (\in_array('\\utf8only', $this->aAttributes)) { } */ } - public function setFlags(array $aFlags) : void + public function setAttributes(array $aAttributes) : void { - $this->aFlagsLowerCase = \array_map('strtolower', $aFlags); + $this->aAttributes = \array_map('mb_strtolower', $aAttributes); + } + + public function setSubscribed() : void + { + $this->aAttributes = \array_unique(\array_merge($this->aAttributes, ['\\subscribed'])); } public function setDelimiter(?string $sDelimiter) : void @@ -72,7 +68,7 @@ class Folder public function Name() : string { - $sNameRaw = $this->sFullName; + $sNameRaw = $this->FullName; if ($this->sDelimiter) { $aNames = \explode($this->sDelimiter, $sNameRaw); return \end($aNames); @@ -80,29 +76,25 @@ class Folder return $sNameRaw; } - public function FullName() : string - { - return $this->sFullName; - } - public function Delimiter() : ?string { return $this->sDelimiter; } - public function FlagsLowerCase() : array + public function Selectable() : bool { - return $this->aFlagsLowerCase; + return !\in_array('\\noselect', $this->aAttributes) + && !\in_array('\\nonexistent', $this->aAttributes); } - public function IsSelectable() : bool + public function IsSubscribed() : bool { - return !\in_array('\\noselect', $this->aFlagsLowerCase) && !\in_array('\\nonexistent', $this->aFlagsLowerCase); + return \in_array('\\subscribed', $this->aAttributes); } public function IsInbox() : bool { - return 'INBOX' === \strtoupper($this->sFullName) || \in_array('\\inbox', $this->aFlagsLowerCase); + return 'INBOX' === \strtoupper($this->FullName) || \in_array('\\inbox', $this->aAttributes); } public function SetMetadata(string $sName, string $sData) : void @@ -124,4 +116,72 @@ class Folder { return $this->aMetadata; } + + // JMAP RFC 8621 + public function Role() : ?string + { + $role = \strtolower($this->GetMetadata(MetadataKeys::SPECIALUSE) ?: ''); + if (!$role) { + $match = \array_intersect([ + '\\inbox', + '\\all', // '\\allmail' + '\\archive', + '\\drafts', + '\\flagged', // '\\starred' + '\\important', + '\\junk', // '\\spam' + '\\sent', // '\\sentmail' + '\\trash', // '\\bin' + ], $this->aAttributes); + if ($match) { + $role = \array_shift($match); + } + if (!$role && 'INBOX' === \strtoupper($this->FullName)) { + return 'inbox'; + } + } + return $role ? \ltrim($role, '\\') : null; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { +/* + if ($this->ImapClient->hasCapability('ACL') || $this->ImapClient->CapabilityValue('RIGHTS')) { + // MailSo\Imap\Responses\ACL + $rights = $this->ImapClient->FolderMyRights($this->FullName); + } +*/ + $result = array( + '@Object' => 'Object/Folder', + 'name' => $this->Name(), + 'fullName' => $this->FullName, + 'delimiter' => (string) $this->sDelimiter, + 'attributes' => $this->aAttributes, + 'metadata' => $this->aMetadata, + 'uidNext' => $this->UIDNEXT, + // https://datatracker.ietf.org/doc/html/rfc8621#section-2 + 'totalEmails' => $this->MESSAGES, + 'unreadEmails' => $this->UNSEEN, + 'id' => $this->MAILBOXID, + 'role' => $this->Role() +/* + 'myRights' => [ + 'mayReadItems' => !$rights || ($rights->hasRight('l') && $rights->hasRight('r')), + 'mayAddItems' => !$rights || $rights->hasRight('i'), + 'mayRemoveItems' => !$rights || ($rights->hasRight('t') && $rights->hasRight('e')), + 'maySetSeen' => !$rights || $rights->hasRight('s'), + 'maySetKeywords' => !$rights || $rights->hasRight('w'), + 'mayCreateChild' => !$rights || $rights->hasRight('k'), + 'mayRename' => !$rights || $rights->hasRight('x'), + 'mayDelete' => !$rights || $rights->hasRight('x'), + 'maySubmit' => !$rights || $rights->hasRight('p') + ] +*/ + ); + if ($this->etag) { + $result['etag'] = $this->etag; + } + return $result; + } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/FolderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderCollection.php similarity index 62% rename from snappymail/v/0.0.0/app/libraries/MailSo/Mail/FolderCollection.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderCollection.php index 4e82c6af3..8e2d9d8c2 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/FolderCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderCollection.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace MailSo\Mail; +namespace MailSo\Imap; /** * @category MailSo @@ -17,12 +17,7 @@ namespace MailSo\Mail; */ class FolderCollection extends \MailSo\Base\Collection { - /** - * @var bool - */ - public $Optimized = false; - - public $TotalCount = 0; +// public bool $Optimized = false; public function append($oFolder, bool $bToTop = false) : void { @@ -30,9 +25,13 @@ class FolderCollection extends \MailSo\Base\Collection parent::append($oFolder, $bToTop); } - public function FindDelimiter() : string + #[\ReturnTypeWillChange] + public function jsonSerialize() { - $oFolder = $this['INBOX'] ?? $this[0] ?? null; - return $oFolder ? $oFolder->Delimiter() : '/'; + return array( + '@Object' => 'Collection/FolderCollection', + '@Collection' => \array_values($this->getArrayCopy()), +// 'optimized' => $this->Optimized + ); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php index c490f3371..a26eab696 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/FolderInformation.php @@ -19,45 +19,32 @@ class FolderInformation implements \JsonSerializable { use Traits\Status; - /** - * @var string - */ - public $FolderName; + public bool $IsWritable; /** - * @var bool - */ - public $IsWritable; - - /** - * @var array * Message flags + * https://www.rfc-editor.org/rfc/rfc3501#section-7.2.6 + * These could be not permanent so we don't use them */ - public $Flags = array(); + public array $Flags = array(); /** - * @var array * NOTE: Empty when FolderExamine is used + * https://www.rfc-editor.org/rfc/rfc3501#page-64 */ - public $PermanentFlags = array(); + public array $PermanentFlags = array(); - /** - * https://datatracker.ietf.org/doc/html/rfc3501#section-7.3.1 - * @var int - */ - public $Exists = null; - - function __construct(string $sFolderName, bool $bIsWritable) + function __construct(string $sFullName, bool $bIsWritable) { - $this->FolderName = $sFolderName; + $this->FullName = $sFullName; $this->IsWritable = $bIsWritable; } public function IsFlagSupported(string $sFlag) : bool { - return \in_array('\\*', $this->PermanentFlags) || - \in_array($sFlag, $this->PermanentFlags) || - \in_array($sFlag, $this->Flags); + return \in_array('\\*', $this->PermanentFlags) + || \in_array($sFlag, $this->PermanentFlags); +// || \in_array($sFlag, $this->Flags); } #[\ReturnTypeWillChange] @@ -65,24 +52,33 @@ class FolderInformation implements \JsonSerializable { $result = array( 'id' => $this->MAILBOXID, - 'Name' => $this->FolderName, - 'Flags' => $this->Flags, - 'PermanentFlags' => $this->PermanentFlags, - 'UidNext' => $this->UIDNEXT, - 'UidValidity' => $this->UIDVALIDITY + 'name' => $this->FullName, + 'uidNext' => $this->UIDNEXT, + 'uidValidity' => $this->UIDVALIDITY ); if (isset($this->MESSAGES)) { $result['totalEmails'] = $this->MESSAGES; $result['unreadEmails'] = $this->UNSEEN; } if (isset($this->HIGHESTMODSEQ)) { - $result['Highestmodseq'] = $this->HIGHESTMODSEQ; + $result['highestModSeq'] = $this->HIGHESTMODSEQ; } if (isset($this->APPENDLIMIT)) { - $result['Appendlimit'] = $this->APPENDLIMIT; + $result['appendLimit'] = $this->APPENDLIMIT; } if (isset($this->SIZE)) { - $result['Size'] = $this->SIZE; + $result['size'] = $this->SIZE; + } + if ($this->etag) { + $result['etag'] = $this->etag; + } +/* + if ($this->Flags) { + $result['flags'] = $this->Flags; + } +*/ + if ($this->PermanentFlags) { + $result['permanentFlags'] = $this->PermanentFlags; } return $result; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php index 615cf2c79..078c91940 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ImapClient.php @@ -26,13 +26,9 @@ class ImapClient extends \MailSo\Net\NetClient use Commands\Metadata; use Commands\Quota; - const - TAG_PREFIX = 'TAG'; + public string $TAG_PREFIX = 'TAG'; - /** - * @var int - */ - private $iTagCount = 0; + private int $iTagCount = 0; /** * @var array @@ -46,39 +42,22 @@ class ImapClient extends \MailSo\Net\NetClient /** * Used by \MailSo\Mail\MailClient::MessageMimeStream - * @var array */ - private $aFetchCallbacks; + private array $aFetchCallbacks = array(); - /** - * @var array - */ - private $aTagTimeouts = array(); + private array $aTagTimeouts = array(); - /** - * @var bool - */ - private $bIsLoggined = false; + private bool $bIsLoggined = false; - /** - * @var string - */ - private $sLogginedUser = ''; + private bool $UTF8 = false; - /** - * @var bool - */ - public $__FORCE_SELECT_ON_EXAMINE__ = false; - public $__DISABLE_METADATA = false; - - /** - * @var bool - */ - private $UTF8 = false; - - public function GetLogginedUser() : string + public function Hash() : string { - return $this->sLogginedUser; + return \md5('ImapClientHash/'. + $this->Settings->Login . '@' . + $this->Settings->host . ':' . + $this->Settings->port + ); } /** @@ -87,6 +66,7 @@ class ImapClient extends \MailSo\Net\NetClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ +// public function Connect(Settings $oSettings) : void public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void { $this->aTagTimeouts['*'] = \microtime(true); @@ -95,63 +75,73 @@ class ImapClient extends \MailSo\Net\NetClient $this->setCapabilities($this->getResponse('*')); - if (ConnectionSecurityType::STARTTLS === $this->iSecurityType - || (ConnectionSecurityType::AUTO_DETECT === $this->iSecurityType && $this->IsSupported('STARTTLS'))) { + if (ConnectionSecurityType::STARTTLS === $this->Settings->type + || (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) { $this->StartTLS(); } } private function StartTLS() : void { - if ($this->IsSupported('STARTTLS')) { + if ($this->hasCapability('STARTTLS')) { $this->SendRequestGetResponse('STARTTLS'); $this->EnableCrypto(); $this->aCapabilityItems = null; } else { $this->writeLogException( new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), - \LOG_ERR, true); + \LOG_ERR); } } + public function supportsAuthType(string $sasl_type) : bool + { + return $this->hasCapability("AUTH={$sasl_type}"); + } + /** * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function Login(array $aCredentials) : self + public function Login(Settings $oSettings) : self { - if (!empty($aCredentials['ProxyAuthUser']) && !empty($aCredentials['ProxyAuthPassword'])) { - $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['ProxyAuthUser'])); - $sPassword = $aCredentials['ProxyAuthPassword']; - $sProxyAuthUser = $aCredentials['Login']; + if ($this->bIsLoggined) { + return $this; + } + + if (!empty($oSettings->ProxyAuthUser) && !empty($oSettings->ProxyAuthPassword)) { + $sLogin = $oSettings->ProxyAuthUser; + $sPassword = $oSettings->ProxyAuthPassword; + $sProxyAuthUser = $oSettings->Login; } else { - $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login'])); - $sPassword = $aCredentials['Password']; + $sLogin = $oSettings->Login; + $sPassword = $oSettings->Password; $sProxyAuthUser = ''; } - if (!\strlen($sLogin) || !\strlen($sPassword)) - { - $this->writeLogException( - new \InvalidArgumentException, - \LOG_ERR, true); + $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sLogin)); + + if (!\strlen($sLogin) || !\strlen($sPassword)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } - $this->sLogginedUser = $sLogin; - - $type = $this->IsSupported('LOGINDISABLED') ? '' : 'LOGIN'; // RFC3501 6.2.3 - foreach ($aCredentials['SASLMechanisms'] as $sasl_type) { - if ($this->IsSupported("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) { + $type = ''; + foreach ($oSettings->SASLMechanisms as $sasl_type) { + if ($this->hasCapability("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) { $type = $sasl_type; break; } } + // RFC3501 6.2.3 + if (!$type && \in_array('LOGIN', $oSettings->SASLMechanisms) && !$this->hasCapability('LOGINDISABLED')) { + $type = 'LOGIN'; + } if (!$type) { - if (!$this->Encrypted() && $this->IsSupported('STARTTLS')) { + if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) { $this->StartTLS(); - return $this->Login($aCredentials); + return $this->Login($oSettings); } throw new \MailSo\RuntimeException('No supported SASL mechanism found, remote server wants: ' . \implode(', ', \array_filter($this->Capability() ?: [], function($var){ @@ -161,16 +151,15 @@ class ImapClient extends \MailSo\Net\NetClient } $SASL = \SnappyMail\SASL::factory($type); - $SASL->base64 = true; try { - if (0 === \strpos($type, 'SCRAM-')) + if (\str_starts_with($type, 'SCRAM-')) { $sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); $this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid*/), true); $sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), Enumerations\ResponseType::CONTINUATION)); - $this->oLogger && $this->oLogger->AddSecret($sChallenge); + $this->logMask($sChallenge); $this->sendRaw($sChallenge); $oResponse = $this->getResponse(); $SASL->verify($this->getResponseValue($oResponse)); @@ -178,17 +167,17 @@ class ImapClient extends \MailSo\Net\NetClient else if ('CRAM-MD5' === $type) { $sChallenge = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); - $this->oLogger->Write('challenge: '.\base64_decode($sChallenge)); + $this->logWrite('challenge: '.\base64_decode($sChallenge)); $sAuth = $SASL->authenticate($sLogin, $sPassword, $sChallenge); - $this->oLogger && $this->oLogger->AddSecret($sAuth); + $this->logMask($sAuth); $this->sendRaw($sAuth); $oResponse = $this->getResponse(); } - else if ('PLAIN' === $type || 'OAUTHBEARER' === $type) + else if ('PLAIN' === $type || 'OAUTHBEARER' === $type /*|| 'PLAIN-CLIENTTOKEN' === $type*/) { $sAuth = $SASL->authenticate($sLogin, $sPassword); - $this->oLogger && $this->oLogger->AddSecret($sAuth); - if ($this->IsSupported('SASL-IR')) { + $this->logMask($sAuth); + if ($this->hasCapability('SASL-IR')) { $oResponse = $this->SendRequestGetResponse('AUTHENTICATE', array($type, $sAuth)); } else { $this->SendRequestGetResponse('AUTHENTICATE', array($type)); @@ -203,27 +192,26 @@ class ImapClient extends \MailSo\Net\NetClient $oR = $oResponse->getLast(); if ($oR && Enumerations\ResponseType::CONTINUATION === $oR->ResponseType) { if (!empty($oR->ResponseList[1]) && preg_match('/^[a-zA-Z0-9=+\/]+$/', $oR->ResponseList[1])) { - $this->Logger()->Write(\base64_decode($oR->ResponseList[1]), - \LOG_WARNING); + $this->logWrite(\base64_decode($oR->ResponseList[1]), \LOG_WARNING); } $this->sendRaw(''); $oResponse = $this->getResponse(); } } - else if ($this->IsSupported('LOGINDISABLED')) + else if ($this->hasCapability('LOGINDISABLED')) { $sB64 = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); $this->sendRaw($SASL->authenticate($sLogin, $sPassword, $sB64), true); $this->getResponse(); $sPass = $SASL->challenge(''/*UGFzc3dvcmQ6*/); - $this->oLogger && $this->oLogger->AddSecret($sPass); + $this->logMask($sPass); $this->sendRaw($sPass); $oResponse = $this->getResponse(); } else { $sPassword = $this->EscapeString(\mb_convert_encoding($sPassword, 'ISO-8859-1', 'UTF-8')); - $this->oLogger && $this->oLogger->AddSecret($sPassword); + $this->logMask($sPassword); $oResponse = $this->SendRequestGetResponse('LOGIN', array( $this->EscapeString($sLogin), @@ -233,28 +221,24 @@ class ImapClient extends \MailSo\Net\NetClient $this->setCapabilities($oResponse); - if (\strlen($sProxyAuthUser)) - { + if (\strlen($sProxyAuthUser)) { $this->SendRequestGetResponse('PROXYAUTH', array($this->EscapeString($sProxyAuthUser))); } /* // TODO: RFC 9051 - if ($this->IsSupported('IMAP4rev2')) { + if ($this->hasCapability('IMAP4rev2')) { $this->Enable('IMAP4rev1'); } */ // RFC 6855 || RFC 5738 - $this->UTF8 = $this->IsSupported('UTF8=ONLY') || $this->IsSupported('UTF8=ACCEPT'); + $this->UTF8 = $this->hasCapability('UTF8=ONLY') || $this->hasCapability('UTF8=ACCEPT'); if ($this->UTF8) { $this->Enable('UTF8=ACCEPT'); } } catch (Exceptions\NegativeResponseException $oException) { - $this->writeLogException( - new Exceptions\LoginBadCredentialsException( - $oException->GetResponses(), '', 0, $oException), - \LOG_NOTICE, true); + $this->writeLogException(new Exceptions\LoginBadCredentialsException($oException->GetResponses(), '', 0, $oException)); } $this->bIsLoggined = true; @@ -269,8 +253,7 @@ class ImapClient extends \MailSo\Net\NetClient */ public function Logout() : void { - if ($this->bIsLoggined) - { + if ($this->bIsLoggined) { $this->bIsLoggined = false; $this->SendRequestGetResponse('LOGOUT'); } @@ -302,12 +285,44 @@ class ImapClient extends \MailSo\Net\NetClient return $this->aCapabilityItems; } + public function CapabilityValue(string $sExtentionName) : ?string + { + $sExtentionName = \trim($sExtentionName) . '='; + $aCapabilities = $this->Capability() ?: []; + foreach ($aCapabilities as $string) { + if (\str_starts_with($string, $sExtentionName)) { + return \substr($string, \strlen($sExtentionName)); + } + } + return null; + } + private function setCapabilities(ResponseCollection $oResponseCollection) : void { $aList = $oResponseCollection->getCapabilityResult(); - if ($aList && $this->__DISABLE_METADATA) { - // Issue #365: Many folders on Cyrus IMAP breaks login - $aList = \array_diff($aList, ['METADATA']); + if ($aList) { + if ($this->Settings->disable_metadata) { + // Issue #365: Many folders on Cyrus IMAP breaks login + $aList = \array_diff($aList, ['METADATA']); + } + if ($this->Settings->disable_move) { + $aList = \array_diff($aList, ['MOVE']); + } + if ($this->Settings->disable_sort) { + $aList = \array_diff($aList, ['SORT','ESORT']); + } + if ($this->Settings->disable_thread) { + $aList = \array_filter($aList, function ($item) { \str_starts_with($item, 'THREAD='); }); + } + if ($this->Settings->disable_list_status) { + $aList = \array_diff($aList, ['LIST-STATUS']); + } + if ($this->Settings->disable_binary) { + $aList = \array_diff($aList, ['BINARY']); + } + if (8 > PHP_INT_SIZE) { + $aList = \array_diff($aList, ['CONDSTORE']); + } } $this->aCapabilityItems = $aList; } @@ -326,11 +341,15 @@ class ImapClient extends \MailSo\Net\NetClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function IsSupported(string $sExtentionName) : bool + public function hasCapability(string $sExtentionName) : bool { $sExtentionName = \trim($sExtentionName); return $sExtentionName && \in_array(\strtoupper($sExtentionName), $this->Capability() ?: []); } + public function IsSupported(string $sExtentionName) : bool + { + return $this->hasCapability($sExtentionName); + } /** * RFC 5161 @@ -352,7 +371,7 @@ class ImapClient extends \MailSo\Net\NetClient */ public function GetNamespace() : ?NamespaceResult { - if (!$this->IsSupported('NAMESPACE')) { + if (!$this->hasCapability('NAMESPACE')) { return null; } @@ -362,18 +381,21 @@ class ImapClient extends \MailSo\Net\NetClient if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType && 'NAMESPACE' === $oResponse->StatusOrIndex) { - $oReturn = new NamespaceResult; - $oReturn->InitByImapResponse($oResponse); - return $oReturn; + return new NamespaceResult($oResponse); } } throw new Exceptions\ResponseException; } catch (\Throwable $e) { $this->writeLogException($e, \LOG_ERR); - throw $e; } } + public function GetPersonalNamespace() : string + { + $oNamespace = $this->GetNamespace(); + return $oNamespace ? $oNamespace->GetPersonalNamespace() : ''; + } + /** * RFC 7889 * APPENDLIMIT= indicates that the IMAP server has the same upload limit for all mailboxes. @@ -383,14 +405,8 @@ class ImapClient extends \MailSo\Net\NetClient */ public function AppendLimit() : ?int { - if ($this->Capability()) { - foreach ($this->aCapabilityItems as $string) { - if ('APPENDLIMIT=' === \substr($string, 0, 12)) { - return (int) \substr($string, 12); - } - } - } - return null; + $string = $this->CapabilityValue('APPENDLIMIT'); + return \is_null($string) ? null : (int) $string; } /** @@ -414,7 +430,7 @@ class ImapClient extends \MailSo\Net\NetClient { $sCommand = \trim($sCommand); if (!\strlen($sCommand)) { - $this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true); + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } $this->IsConnected(true); @@ -457,13 +473,9 @@ class ImapClient extends \MailSo\Net\NetClient if ($sResult) { return $sResult; } - $this->writeLogException( - new Exceptions\LoginException, - \LOG_NOTICE, true); + $this->writeLogException(new Exceptions\LoginException); } - $this->writeLogException( - new Exceptions\LoginException, - \LOG_NOTICE, true); + $this->writeLogException(new Exceptions\LoginException); } /** @@ -494,7 +506,6 @@ class ImapClient extends \MailSo\Net\NetClient } } catch (\Throwable $e) { $this->writeLogException($e, \LOG_WARNING); - throw $e; } } @@ -532,7 +543,6 @@ class ImapClient extends \MailSo\Net\NetClient } catch (\Throwable $e) { $this->writeLogException($e, \LOG_WARNING); - throw $e; } return $oResult; @@ -577,21 +587,16 @@ class ImapClient extends \MailSo\Net\NetClient } catch (\Throwable $e) { $this->writeLogException($e, \LOG_WARNING); - throw $e; } } protected function prepareParamLine(array $aParams = array()) : string { $sReturn = ''; - foreach ($aParams as $mParamItem) - { - if (\is_array($mParamItem) && \count($mParamItem)) - { + foreach ($aParams as $mParamItem) { + if (\is_array($mParamItem) && \count($mParamItem)) { $sReturn .= ' ('.\trim($this->prepareParamLine($mParamItem)).')'; - } - else if (\is_string($mParamItem)) - { + } else if (\is_string($mParamItem)) { $sReturn .= ' '.$mParamItem; } } @@ -606,7 +611,7 @@ class ImapClient extends \MailSo\Net\NetClient protected function getCurrentTag() : string { - return self::TAG_PREFIX.$this->iTagCount; + return $this->TAG_PREFIX.$this->iTagCount; } public function EscapeString(?string $sStringForEscape) : string @@ -616,6 +621,7 @@ class ImapClient extends \MailSo\Net\NetClient } /* // literal-string + $this->hasCapability('LITERAL+') if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) { return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape); } @@ -635,7 +641,7 @@ class ImapClient extends \MailSo\Net\NetClient */ public function ServerID() : string { - if ($this->IsSupported('ID')) { + if ($this->hasCapability('ID')) { foreach ($this->SendRequestGetResponse('ID', [null]) as $oResponse) { if ('ID' === $oResponse->ResponseList[1] && \is_array($oResponse->ResponseList[2])) { $c = \count($oResponse->ResponseList[2]); @@ -657,7 +663,7 @@ class ImapClient extends \MailSo\Net\NetClient public function Compress() : bool { try { - if ($this->IsSupported('COMPRESS=DEFLATE')) { + if ($this->hasCapability('COMPRESS=DEFLATE')) { $this->SendRequestGetResponse('COMPRESS', ['DEFLATE']); \stream_filter_append($this->ConnectionResource(), 'zlib.inflate'); \stream_filter_append($this->ConnectionResource(), 'zlib.deflate', STREAM_FILTER_WRITE, array( diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php index 8705cd548..b5e92a000 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/NamespaceResult.php @@ -17,75 +17,38 @@ namespace MailSo\Imap; */ class NamespaceResult { - /** - * @var string - */ - private $sPersonal = ''; + private string $sPersonal = ''; - /** - * @var string - */ - private $sPersonalDelimiter = ''; + private string $sPersonalDelimiter = ''; +/* + private string $sOtherUser = ''; - /** - * @var string - */ - private $sOtherUser = ''; + private string $sOtherUserDelimiter = ''; - /** - * @var string - */ - private $sOtherUserDelimiter = ''; + private string $sShared = ''; - /** - * @var string - */ - private $sShared = ''; - - /** - * @var string - */ - private $sSharedDelimiter = ''; - - public function InitByImapResponse(\MailSo\Imap\Response $oImapResponse) : self + private string $sSharedDelimiter = ''; +*/ + function __construct(Response $oImapResponse) { - if ($oImapResponse) - { - if (isset($oImapResponse->ResponseList[2][0]) && - \is_array($oImapResponse->ResponseList[2][0]) && - 2 <= \count($oImapResponse->ResponseList[2][0])) - { - $this->sPersonal = $oImapResponse->ResponseList[2][0][0]; - $this->sPersonalDelimiter = $oImapResponse->ResponseList[2][0][1]; - - $this->sPersonal = 'INBOX'.$this->sPersonalDelimiter === \substr(\strtoupper($this->sPersonal), 0, 6) ? - 'INBOX'.$this->sPersonalDelimiter.\substr($this->sPersonal, 6) : $this->sPersonal; - } - - if (isset($oImapResponse->ResponseList[3][0]) && - \is_array($oImapResponse->ResponseList[3][0]) && - 2 <= \count($oImapResponse->ResponseList[3][0])) - { - $this->sOtherUser = $oImapResponse->ResponseList[3][0][0]; - $this->sOtherUserDelimiter = $oImapResponse->ResponseList[3][0][1]; - - $this->sOtherUser = 'INBOX'.$this->sOtherUserDelimiter === \substr(\strtoupper($this->sOtherUser), 0, 6) ? - 'INBOX'.$this->sOtherUserDelimiter.\substr($this->sOtherUser, 6) : $this->sOtherUser; - } - - if (isset($oImapResponse->ResponseList[4][0]) && - \is_array($oImapResponse->ResponseList[4][0]) && - 2 <= \count($oImapResponse->ResponseList[4][0])) - { - $this->sShared = $oImapResponse->ResponseList[4][0][0]; - $this->sSharedDelimiter = $oImapResponse->ResponseList[4][0][1]; - - $this->sShared = 'INBOX'.$this->sSharedDelimiter === \substr(\strtoupper($this->sShared), 0, 6) ? - 'INBOX'.$this->sSharedDelimiter.\substr($this->sShared, 6) : $this->sShared; - } + $space = static::getNamespace($oImapResponse, 2); + if ($space) { + $this->sPersonal = $space[0]; + $this->sPersonalDelimiter = $space[1]; + } +/* + $space = static::getNamespace($oImapResponse, 3); + if ($space) { + $this->sOtherUser = $space[0]; + $this->sOtherUserDelimiter = $space[1]; } - return $this; + $space = static::getNamespace($oImapResponse, 4); + if ($space) { + $this->sShared = $space[0]; + $this->sSharedDelimiter = $space[1]; + } +*/ } public function GetPersonalNamespace() : string @@ -93,4 +56,20 @@ class NamespaceResult return $this->sPersonal; } + private static function getNamespace(Response $oImapResponse, int $section) : ?array + { + if (isset($oImapResponse->ResponseList[$section][0]) + && \is_array($oImapResponse->ResponseList[$section][0]) + && 2 <= \count($oImapResponse->ResponseList[$section][0])) + { + $sName = $oImapResponse->ResponseList[$section][0][0]; + $sDelimiter = $oImapResponse->ResponseList[$section][0][1]; + $sName = 'INBOX'.$sDelimiter === \substr(\strtoupper($sName), 0, 6) + ? 'INBOX'.$sDelimiter.\substr($sName, 6) + : $sName; + return [$sName, $sDelimiter]; + } + return null; + } + } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php index 852c65043..9a51f8bd5 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/ESEARCH.php @@ -18,9 +18,9 @@ namespace MailSo\Imap\Requests; */ class ESEARCH extends Request { - public - $sCriterias = 'ALL', - $aReturn = [ + public string $sCriterias = 'ALL'; + + public array $aReturn = [ /** ALL Return all message numbers/UIDs which match the search criteria, @@ -41,21 +41,23 @@ class ESEARCH extends Request Return all message numbers/UIDs which match the search criteria, in the requested sort order, using a sequence-set. */ - ], - $bUid = true, - $sLimit = '', - $sCharset = '', - // https://datatracker.ietf.org/doc/html/rfc7377 - $aMailboxes = [], - $aSubtrees = [], - $aSubtreesOne = []; + ]; + + public bool $bUid = true; + + public string $sLimit = ''; + + public string $sCharset = ''; + + // https://datatracker.ietf.org/doc/html/rfc7377 + public array $aMailboxes = []; + public array $aSubtrees = []; + public array $aSubtreesOne = []; function __construct(\MailSo\Imap\ImapClient $oImapClient) { - if (!$oImapClient->IsSupported('ESEARCH')) { - $oImapClient->writeLogException( - new \MailSo\RuntimeException('ESEARCH is not supported'), - \LOG_ERR, true); + if (!$oImapClient->hasCapability('ESEARCH')) { + $oImapClient->writeLogException(new \MailSo\RuntimeException('ESEARCH is not supported'), \LOG_ERR); } parent::__construct($oImapClient); } @@ -66,10 +68,8 @@ class ESEARCH extends Request $aRequest = array(); /* // RFC 6203 - if (false !== \stripos($this->sCriterias, 'FUZZY') && !$this->oImapClient->IsSupported('SEARCH=FUZZY')) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException('SEARCH=FUZZY is not supported'), - \LOG_ERR, true); + if (false !== \stripos($this->sCriterias, 'FUZZY') && !$this->oImapClient->hasCapability('SEARCH=FUZZY')) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('SEARCH=FUZZY is not supported'), \LOG_ERR); } */ @@ -87,10 +87,8 @@ class ESEARCH extends Request $aFolders[] = $this->aSubtreesOne; } if ($aFolders) { - if (!$this->oImapClient->IsSupported('MULTISEARCH')) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException('MULTISEARCH is not supported'), - \LOG_ERR, true); + if (!$this->oImapClient->hasCapability('MULTISEARCH')) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('MULTISEARCH is not supported'), \LOG_ERR); } $sCmd = 'ESEARCH'; $aReques[] = 'IN'; @@ -105,12 +103,10 @@ class ESEARCH extends Request $aRequest[] = 'RETURN'; if ($this->aReturn) { // RFC 5267 checks - if (!$this->oImapClient->IsSupported('CONTEXT=SEARCH')) { + if (!$this->oImapClient->hasCapability('CONTEXT=SEARCH')) { foreach ($this->aReturn as $sReturn) { if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException('CONTEXT=SEARCH is not supported'), - \LOG_ERR, true); + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('CONTEXT=SEARCH is not supported'), \LOG_ERR); } } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php index ec5e11f1e..0f8ef9b7e 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/Request.php @@ -13,8 +13,7 @@ namespace MailSo\Imap\Requests; abstract class Request { - protected - $oImapClient; + protected \MailSo\Imap\ImapClient $oImapClient; function __construct(\MailSo\Imap\ImapClient $oImapClient) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php index 76fc2386f..7e4e28e64 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/SORT.php @@ -21,14 +21,18 @@ namespace MailSo\Imap\Requests; */ class SORT extends Request { - public - $sCriterias = 'ALL', - $sCharset = '', - $bUid = true, - $aSortTypes = [], - $sLimit = '', - // RFC 5267 - $aReturn = [ + public string $sCriterias = 'ALL'; + + public string $sCharset = ''; + + public bool $bUid = true; + + public array $aSortTypes = []; + + public string $sLimit = ''; + + // RFC 5267 + public array $aReturn = [ /** ALL Return all message numbers/UIDs which match the search criteria, @@ -53,10 +57,8 @@ class SORT extends Request function __construct(\MailSo\Imap\ImapClient $oImapClient) { - if (!$oImapClient->IsSupported('SORT')) { - $oImapClient->writeLogException( - new \MailSo\RuntimeException('SORT is not supported'), - \LOG_ERR, true); + if (!$oImapClient->hasCapability('SORT')) { + $oImapClient->writeLogException(new \MailSo\RuntimeException('SORT is not supported'), \LOG_ERR); } parent::__construct($oImapClient); } @@ -64,26 +66,20 @@ class SORT extends Request public function SendRequest() : string { if (!$this->aSortTypes) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException('SortTypes are missing'), - \LOG_ERR, true); + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('SortTypes are missing'), \LOG_ERR); } $aRequest = array(); if ($this->aReturn) { // RFC 5267 checks - if (!$this->oImapClient->IsSupported('ESORT')) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException('ESORT is not supported'), - \LOG_ERR, true); + if (!$this->oImapClient->hasCapability('ESORT')) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('ESORT is not supported'), \LOG_ERR); } - if (!$this->oImapClient->IsSupported('CONTEXT=SORT')) { + if (!$this->oImapClient->hasCapability('CONTEXT=SORT')) { foreach ($this->aReturn as $sReturn) { if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException('CONTEXT=SORT is not supported'), - \LOG_ERR, true); + $this->oImapClient->writeLogException(new \MailSo\RuntimeException('CONTEXT=SORT is not supported'), \LOG_ERR); } } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php index e628f2b23..9175c087b 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Requests/THREAD.php @@ -20,35 +20,38 @@ namespace MailSo\Imap\Requests; */ class THREAD extends Request { - public - $sAlgorithm = '', // ORDEREDSUBJECT or REFERENCES or REFS - $sCriterias = 'ALL', - $bUid = true; + // ORDEREDSUBJECT or REFERENCES or REFS + private string $sAlgorithm = ''; + + public string $sCriterias = 'ALL'; + + public bool $bUid = true; function __construct(\MailSo\Imap\ImapClient $oImapClient) { - if ($oImapClient->IsSupported('THREAD=REFS')) { + if ($oImapClient->hasCapability('THREAD=REFS')) { $this->sAlgorithm = 'REFS'; - } else if ($oImapClient->IsSupported('THREAD=REFERENCES')) { + } else if ($oImapClient->hasCapability('THREAD=REFERENCES')) { $this->sAlgorithm = 'REFERENCES'; - } else if ($oImapClient->IsSupported('THREAD=ORDEREDSUBJECT')) { + } else if ($oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')) { $this->sAlgorithm = 'ORDEREDSUBJECT'; } else { - $oImapClient->writeLogException( - new \MailSo\RuntimeException('THREAD is not supported'), - \LOG_ERR, true); + $oImapClient->writeLogException(new \MailSo\RuntimeException('THREAD is not supported'), \LOG_ERR); } parent::__construct($oImapClient); } + public function setAlgorithm(string $sAlgorithm) : void + { + $sAlgorithm = \strtoupper($sAlgorithm); + if (!$this->oImapClient->hasCapability("THREAD={$sAlgorithm}")) { + $this->oImapClient->writeLogException(new \MailSo\RuntimeException("THREAD={$sAlgorithm} is not supported"), \LOG_ERR); + } + $this->sAlgorithm = $sAlgorithm; + } + public function SendRequestIterateResponse() : iterable { - if (!$this->oImapClient->IsSupported(\strtoupper("THREAD={$this->sAlgorithm}"))) { - $this->oImapClient->writeLogException( - new \MailSo\RuntimeException("THREAD={$this->sAlgorithm} is not supported"), - \LOG_ERR, true); - } - $this->oImapClient->SendRequest( ($this->bUid ? 'UID THREAD' : 'THREAD'), array( diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php index 1afc32c08..1c76dfb2e 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Response.php @@ -19,46 +19,24 @@ use MailSo\Imap\Enumerations\ResponseType; */ class Response { - /** - * @var array - */ - public $ResponseList = array(); + public array $ResponseList = array(); - /** - * @var array | null - */ - public $OptionalResponse = null; + public ?array $OptionalResponse = null; - /** - * @var string - */ - public $StatusOrIndex; + public ?string $StatusOrIndex = null; - /** - * @var string - */ - public $HumanReadable = ''; + public string $HumanReadable = ''; - /** - * @var bool - */ - public $IsStatusResponse = false; + public bool $IsStatusResponse = false; - /** - * @var int - */ - public $ResponseType = 0; + public int $ResponseType = 0; - /** - * @var string - */ - public $Tag; + public ?string $Tag = null; private function recToLine(array $aList) : string { $aResult = array(); - foreach ($aList as $mItem) - { + foreach ($aList as $mItem) { $aResult[] = \is_array($mItem) ? '('.$this->recToLine($mItem).')' : (string) $mItem; } return \implode(' ', $aResult); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php index 9cd577f98..ba83a0247 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/ResponseCollection.php @@ -67,9 +67,7 @@ class ResponseCollection extends \MailSo\Base\Collection } if (\is_array($aList) && \count($aList)) { - $aList = \array_map('strtoupper', $aList); - \sort($aList); - return $aList; + return \array_map('strtoupper', $aList); } } return null; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php index 2cfd8b509..3da0a0be9 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Responses/ACL.php @@ -17,7 +17,7 @@ namespace MailSo\Imap\Responses; */ class ACL implements \JsonSerializable { - private $rights; + private array $rights; function __construct(array $rights) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php index 6314f6172..926f1177f 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SearchCriterias.php @@ -19,8 +19,7 @@ namespace MailSo\Imap; abstract class SearchCriterias { const - RegEx = 'in|e?mail|from|to|subject|has|is|date|since|before|text|body|size|larger|bigger|smaller|maxsize|minsize|keyword|older_than|newer_than'; - + RegEx = 'in|e?mail|from|to|subject|has|is|date|since|before|text|body|size|larger|bigger|smaller|maxsize|minsize|keyword|older_than|newer_than|on|senton|sentsince|sentbefore'; /** https://datatracker.ietf.org/doc/html/rfc3501#section-6.4.4 @@ -66,22 +65,22 @@ abstract class SearchCriterias ☐ NOT Messages that do not match the specified search key. - ☐ ON + ✔ ON Messages whose internal date (disregarding time and timezone) is within the specified date. ✔ OR Messages that match either search key. - ☐ SENTBEFORE + ✔ SENTBEFORE Messages whose [RFC-2822] Date: header (disregarding time and timezone) is earlier than the specified date. - ☐ SENTON + ✔ SENTON Messages whose [RFC-2822] Date: header (disregarding time and timezone) is within the specified date. - ☐ SENTSINCE + ✔ SENTSINCE Messages whose [RFC-2822] Date: header (disregarding time and timezone) is within or later than the specified date. @@ -132,11 +131,6 @@ abstract class SearchCriterias $iTimeFilter = 0; $aCriteriasResult = array(); - if (0 < \MailSo\Config::$MessageListDateFilter) { - $iD = \time() - 3600 * 24 * 30 * \MailSo\Config::$MessageListDateFilter; - $iTimeFilter = \gmmktime(1, 1, 1, \gmdate('n', $iD), 1, \gmdate('Y', $iD)); - } - $sSearch = \trim($sSearch); if (\strlen($sSearch)) { $aLines = \preg_match('/^('.static::RegEx.'):/i', $sSearch) @@ -146,7 +140,7 @@ abstract class SearchCriterias if (!$aLines) { $sValue = static::escapeSearchString($oImapClient, $sSearch); - if (\MailSo\Config::$MessageListFastSimpleSearch) { + if ($oImapClient->Settings->fast_simple_search) { $aCriteriasResult[] = 'OR OR OR'; $aCriteriasResult[] = 'FROM'; $aCriteriasResult[] = $sValue; @@ -161,7 +155,7 @@ abstract class SearchCriterias $aCriteriasResult[] = $sValue; } } else { - if (isset($aLines['IN']) && $oImapClient->IsSupported('MULTISEARCH') && \in_array($aLines['IN'], ['subtree','subtree-one','mailboxes'])) { + if (isset($aLines['IN']) && $oImapClient->hasCapability('MULTISEARCH') && \in_array($aLines['IN'], ['subtree','subtree-one','mailboxes'])) { $aCriteriasResult[] = "IN ({$aLines['IN']} \"{$sFolderName}\")"; } @@ -242,10 +236,14 @@ abstract class SearchCriterias $iTimeFilter = \max($iTimeFilter, $sValue); } break; + case 'ON': + case 'SENTON': + case 'SENTSINCE': + case 'SENTBEFORE': case 'BEFORE': $sValue = static::parseSearchDate($sRawValue); if ($sValue) { - $aCriteriasResult[] = 'BEFORE'; + $aCriteriasResult[] = $sName; $aCriteriasResult[] = \gmdate('j-M-Y', $sValue); } break; @@ -307,8 +305,8 @@ abstract class SearchCriterias $aCriteriasResult[] = 'UNDELETED'; } - if (\MailSo\Config::$MessageListPermanentFilter) { - $aCriteriasResult[] = \MailSo\Config::$MessageListPermanentFilter; + if ($oImapClient->Settings->search_filter) { + $aCriteriasResult[] = $oImapClient->Settings->search_filter; } $sCriteriasResult = \trim(\implode(' ', $aCriteriasResult)); @@ -318,8 +316,12 @@ abstract class SearchCriterias public static function escapeSearchString(\MailSo\Imap\ImapClient $oImapClient, string $sSearch) : string { - return !\MailSo\Base\Utils::IsAscii($sSearch) - ? '{'.\strlen($sSearch).'}'."\r\n".$sSearch : $oImapClient->EscapeString($sSearch); + // https://github.com/the-djmaze/snappymail/issues/836 +// return $oImapClient->EscapeString($sSearch); +// return \MailSo\Base\Utils::IsAscii($sSearch) || $oImapClient->hasCapability('QQMail')) + return (\MailSo\Base\Utils::IsAscii($sSearch) || !$oImapClient->hasCapability('LITERAL+')) + ? $oImapClient->EscapeString($sSearch) + : '{'.\strlen($sSearch).'}'."\r\n{$sSearch}"; } private static function parseSearchDate(string $sDate) : int @@ -379,6 +381,10 @@ abstract class SearchCriterias case 'SMALLER': case 'LARGER': case 'SINCE': + case 'ON': + case 'SENTON': + case 'SENTSINCE': + case 'SENTBEFORE': case 'BEFORE': if (\strlen($mValue)) { $aResult[$sName] = $mValue; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php index 538cb6fc2..d5ac526ac 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/SequenceSet.php @@ -24,9 +24,9 @@ class SequenceSet /*extends \SplFixedArray*/ implements \Countable /** * By default the numbers are unique identifiers as this is more reliable. */ - public $UID = true; + public bool $UID = true; - private $data = []; + private array $data = []; public function __construct($mItems, bool $uid = true) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Settings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Settings.php new file mode 100644 index 000000000..c50d3767b --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Settings.php @@ -0,0 +1,106 @@ +$option = !empty($aSettings[$option]); + } + } + $options = [ +// 'body_text_limit', +// 'folder_list_limit', + 'message_list_limit', +// 'thread_limit', + ]; + foreach ($options as $option) { + if (isset($aSettings[$option])) { + $object->$option = \intval($aSettings[$option]); + } + } + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_merge( + parent::jsonSerialize(), + [ +// '@Object' => 'Object/ImapSettings', + 'disable_list_status' => $this->disable_list_status, + 'disable_metadata' => $this->disable_metadata, + 'disable_move' => $this->disable_move, + 'disable_sort' => $this->disable_sort, + 'disable_thread' => $this->disable_thread, + 'disable_binary' => $this->disable_binary, + 'use_expunge_all_on_delete' => $this->expunge_all_on_delete, +// 'body_text_limit' => $this->body_text_limit, + 'fast_simple_search' => $this->fast_simple_search, +// 'folder_list_limit' => $this->folder_list_limit, + 'force_select' => $this->force_select, + 'message_all_headers' => $this->message_all_headers, + 'message_list_limit' => $this->message_list_limit, + 'search_filter' => $this->search_filter, +// 'thread_limit' => $this->thread_limit + ] + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php index 44636cffe..d7fb040ea 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/ResponseParser.php @@ -21,15 +21,9 @@ use MailSo\Imap\Exceptions\ResponseNotFoundException; */ trait ResponseParser { - /** - * @var int - */ - private $iResponseBufParsedPos; + private int $iResponseBufParsedPos; - /** - * @var bool - */ - private $bNeedNext = true; + private bool $bNeedNext = true; protected function partialParseResponse() : Response { @@ -66,6 +60,8 @@ trait ResponseParser return !$oImapResponse->IsStatusResponse || 2 < \count($oImapResponse->ResponseList); } + private $sResponseBuffer = ''; + /** * @return array|string * @throws \MailSo\RuntimeException @@ -87,35 +83,25 @@ trait ResponseParser $sAtomBuilder = $bTreatAsAtom ? '' : null; $aList = array(); - if ($bRoot) - { + if ($bRoot) { $aList =& $oImapResponse->ResponseList; } - while (true) - { - if ($this->bNeedNext) - { - /** - * $this->sResponseBuffer is a single fgets() that ends with \r\n - */ + while (true) { + if ($this->bNeedNext) { $iPos = 0; - $this->getNextBuffer(); + $this->sResponseBuffer = $this->getNextBuffer(); $this->iResponseBufParsedPos = $iPos; $this->bNeedNext = false; } $sChar = null; - if ($bIsGotoDefault) - { + if ($bIsGotoDefault) { $bIsGotoDefault = false; - } - else - { + } else { $iBufferEndIndex = \strlen($this->sResponseBuffer) - 3; - if ($iPos > $iBufferEndIndex) - { + if ($iPos > $iBufferEndIndex) { break; } @@ -185,9 +171,7 @@ trait ResponseParser if (null !== $sLiteral) { if (!$bTreatAsAtom) { $aList[] = $sLiteral; - if (\MailSo\Config::$LogSimpleLiterals) { - $this->writeLog('{'.$iLiteralLen.'} '.$sLiteral, \LOG_INFO); - } +// $this->writeLog('{'.$iLiteralLen.'} '.$sLiteral, \LOG_INFO); } else { \SnappyMail\Log::notice('IMAP', 'Literal treated as atom and skipped'); } @@ -255,16 +239,14 @@ trait ResponseParser default: $iCharBlockStartPos = $iPos; - if ($bRoot && $oImapResponse->IsStatusResponse) - { + if ($bRoot && $oImapResponse->IsStatusResponse) { $iPos = $iBufferEndIndex; if ($iPos > $iCharBlockStartPos) { $iCharBlockStartPos += \strspn($this->sResponseBuffer, ' ', $iCharBlockStartPos, $iPos - $iCharBlockStartPos); } } - while ($iPos <= $iBufferEndIndex) - { + while ($iPos <= $iBufferEndIndex) { $sCharDef = $this->sResponseBuffer[$iPos]; switch (true) { @@ -272,8 +254,7 @@ trait ResponseParser ++$iPos; break; case '[' === $sCharDef: - if (null === $sAtomBuilder) - { + if (null === $sAtomBuilder) { $sAtomBuilder = ''; } @@ -285,8 +266,7 @@ trait ResponseParser $oImapResponse, true, $sPreviousAtomUpperCase, '[' ); - if (null !== $sListBlock) - { + if (null !== $sListBlock) { $sAtomBuilder .= $sListBlock.']'; } @@ -303,33 +283,25 @@ trait ResponseParser } } - if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder) - { + if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder) { $sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos); - if (null === $sAtomBuilder) - { + if (null === $sAtomBuilder) { $aList[] = 'NIL' === $sLastCharBlock ? null : $sLastCharBlock; $sPreviousAtomUpperCase = \strtoupper($sLastCharBlock); - } - else - { + } else { $sAtomBuilder .= $sLastCharBlock; - if (!$bTreatAsAtom) - { + if (!$bTreatAsAtom) { $aList[] = $sAtomBuilder; $sPreviousAtomUpperCase = \strtoupper($sAtomBuilder); $sAtomBuilder = null; } } - if ($bRoot) - { - if (!isset($oImapResponse->Tag) && 1 === \count($aList)) - { + if ($bRoot) { + if (!isset($oImapResponse->Tag) && 1 === \count($aList)) { $oImapResponse->setTag($aList[0]); - if ($this->getCurrentTag() === $oImapResponse->Tag) - { + if ($this->getCurrentTag() === $oImapResponse->Tag) { $oImapResponse->ResponseType = ResponseType::TAGGED; } } @@ -374,8 +346,7 @@ trait ResponseParser $iLiteralSize = \strlen($sLiteral); if ($iLiteralLen !== $iLiteralSize) { - $this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '. - $iLiteralLen.'" bytes', \LOG_WARNING); + $this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.$iLiteralLen.'" bytes', \LOG_WARNING); } return $sLiteral; } @@ -387,9 +358,9 @@ trait ResponseParser } $sLiteralAtomUpperCasePeek = ''; - if (0 === \strpos($sLiteralAtomUpperCase, 'BODY')) { + if (\str_starts_with($sLiteralAtomUpperCase, 'BODY')) { $sLiteralAtomUpperCasePeek = \str_replace('BODY', 'BODY.PEEK', $sLiteralAtomUpperCase); - } else if (0 === \strpos($sLiteralAtomUpperCase, 'BINARY')) { + } else if (\str_starts_with($sLiteralAtomUpperCase, 'BINARY')) { $sLiteralAtomUpperCasePeek = \str_replace('BINARY', 'BINARY.PEEK', $sLiteralAtomUpperCase); } @@ -401,8 +372,7 @@ trait ResponseParser return false; } - $rImapLiteralStream = - \MailSo\Base\StreamWrappers\Literal::CreateStream($this->ConnectionResource(), $iLiteralLen); + $rImapLiteralStream = \MailSo\Base\StreamWrappers\Literal::CreateStream($this->ConnectionResource(), $iLiteralLen); $this->writeLog('Start Callback for '.$sParent.' / '.$sLiteralAtomUpperCase. ' - try to read '.$iLiteralLen.' bytes.', \LOG_INFO); @@ -411,14 +381,14 @@ trait ResponseParser try { - $this->aFetchCallbacks[$sFetchKey]($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream); + $this->aFetchCallbacks[$sFetchKey]($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream, $iLiteralLen); } catch (\Throwable $oException) { - $this->writeLogException($oException); + $this->writeLogException($oException, \LOG_NOTICE, false); } - if ($rImapLiteralStream) { + if (\is_resource($rImapLiteralStream)) { $iNotReadLiteralLen = 0; $bFeof = \feof($rImapLiteralStream); @@ -445,12 +415,10 @@ trait ResponseParser \fclose($rImapLiteralStream); if (0 < $iNotReadLiteralLen) { - $this->writeLog('Not read literal size is '.$iNotReadLiteralLen.' bytes.', - \LOG_WARNING); + $this->writeLog('Not read literal size is '.$iNotReadLiteralLen.' bytes.', \LOG_WARNING); } } else { - $this->writeLog('Literal stream is not resource after callback.', - \LOG_WARNING); + $this->writeLog('Literal stream is not resource after callback.', \LOG_WARNING); } $this->bRunningCallback = false; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php index 36ec68828..1a3f2f612 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/Traits/Status.php @@ -22,84 +22,94 @@ namespace MailSo\Imap\Traits; */ trait Status { - public - /** - * The number of messages in the mailbox. - * This response is named EXISTS as a result of a SELECT or EXAMINE command. - * @var int - */ - $MESSAGES, + public string $FullName; - /** - * The number of messages with the \Recent flag set. - * This response also occurs as a result of a SELECT or EXAMINE command. - * IMAP4rev2 deprecated. - * @var int - */ - $RECENT, + public bool $hasStatus = false; - /** - * The next unique identifier value of the mailbox. - * A 32-bit value - * This response also occurs as a result of a SELECT or EXAMINE command. - * @var int - */ - $UIDNEXT, + /** + * The number of messages in the mailbox. + * This response is named EXISTS as a result of a SELECT or EXAMINE command. + */ + public ?int $MESSAGES = null; - /** - * The unique identifier validity value of the mailbox. - * This response also occurs as a result of a SELECT or EXAMINE command. - * @var int - */ - $UIDVALIDITY, + /** + * The number of messages with the \Recent flag set. + * This response also occurs as a result of a SELECT or EXAMINE command. + * IMAP4rev2 deprecated. + */ + public ?int $RECENT = null; - /** - * The number of messages which do not have the \Seen flag set. - * This response also occurs as a result of a IMAP4rev1 SELECT or EXAMINE command, - * but then it is the message sequence number of the first unseen message. - * IMAP4rev2 deprecated on SELECT/EXAMINE. - * @var int - */ - $UNSEEN, + /** + * The next unique identifier value of the mailbox. + * A 32-bit value + * This response also occurs as a result of a SELECT or EXAMINE command. + */ + public ?int $UIDNEXT = null; - /** - * RFC 4551 - * The highest mod-sequence value of all messages in the mailbox. - * This response also occurs as a result of a SELECT or EXAMINE command. - * @var int 1*DIGIT Positive unsigned 64-bit integer - */ - $HIGHESTMODSEQ, + /** + * The unique identifier validity value of the mailbox. + * This response also occurs as a result of a SELECT or EXAMINE command. + */ + public ?int $UIDVALIDITY = null; - /** - * RFC 7889 - * Message upload size limit. - * @var int - */ - $APPENDLIMIT, + /** + * The number of messages which do not have the \Seen flag set. + * This response also occurs as a result of a IMAP4rev1 SELECT or EXAMINE command, + * but then it is the message sequence number of the first unseen message. + * IMAP4rev2 deprecated on SELECT/EXAMINE. + */ + public ?int $UNSEEN = null; - /** - * RFC 8474 - * A server-allocated unique identifier for the mailbox. - * This response also occurs as a result of a CREATE, SELECT or EXAMINE command. - * @var string - */ - $MAILBOXID, + /** + * RFC 4551 + * The highest mod-sequence value of all messages in the mailbox. + * This response also occurs as a result of a SELECT or EXAMINE command. + * 1*DIGIT Positive unsigned 64-bit integer + */ + public ?int $HIGHESTMODSEQ = null; - /** - * RFC 9051 - * The total size of the mailbox in octets. - * @var int - */ - $SIZE; + /** + * RFC 7889 + * Message upload size limit. + */ + public ?int $APPENDLIMIT = null; - public function getStatusItems() : array + /** + * RFC 8474 + * A server-allocated unique identifier for the mailbox. + * This response also occurs as a result of a CREATE, SELECT or EXAMINE command. + */ + public ?string $MAILBOXID = null; + + /** + * RFC 9051 + * The total size of the mailbox in octets. + */ + public ?int $SIZE = null; + + public ?string $etag = null; + public function generateETag(\MailSo\Imap\ImapClient $oImapClient) : void { - return \array_filter(\get_object_vars($this), function($v, $k){ - return \property_exists(__TRAIT__, $k); - }, ARRAY_FILTER_USE_BOTH); + if (!$this->hasStatus) { + // UNSEEN undefined when only SELECT/EXAMINE is used + \error_log("{$this->FullName} STATUS missing " . \print_r(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true)); + return; + } + if (!isset($this->MESSAGES, $this->UIDNEXT)) { + return; + } + $this->etag = \md5('FolderHash/'. \implode('-', [ + $this->FullName, + $this->MESSAGES, + $this->UIDNEXT, + $this->UIDVALIDITY, + $this->UNSEEN, + $this->HIGHESTMODSEQ, + $oImapClient->Hash() + ])); } - public function setStatus(string $name, $value) : bool + private function setStatusItem(string $name, $value) : bool { if ('EXISTS' === $name) { $name = 'MESSAGES'; @@ -107,10 +117,11 @@ trait Status $name = 'MAILBOXID'; } if (\property_exists(__TRAIT__, $name)) { - if ('MAILBOXID' !== $name) { - $value = (int) $value; + if ('MAILBOXID' === $name) { + $this->MAILBOXID = \base64_encode(\is_array($value) ? $value[0] : $value); + } else { + $this->$name = (int) $value; } - $this->$name = $value; return true; } return false; @@ -137,7 +148,7 @@ trait Status // OK untagged responses if (\is_array($oResponse->OptionalResponse)) { if (\count($oResponse->OptionalResponse) > 1) { - $bResult = $this->setStatus($oResponse->OptionalResponse[0], $oResponse->OptionalResponse[1]); + $bResult = $this->setStatusItem($oResponse->OptionalResponse[0], $oResponse->OptionalResponse[1]); } } @@ -149,16 +160,18 @@ trait Status && \is_array($oResponse->ResponseList[3])) { $c = \count($oResponse->ResponseList[3]); for ($i = 0; $i < $c; $i += 2) { - $bResult |= $this->setStatus( + $bResult |= $this->setStatusItem( $oResponse->ResponseList[3][$i], $oResponse->ResponseList[3][$i+1] ); } + $this->hasStatus = $bResult; } // SELECT or EXAMINE command else if (\is_numeric($oResponse->ResponseList[1]) && \is_string($oResponse->ResponseList[2])) { + // UNSEEN deprecated in IMAP4rev2 if ('UNSEEN' !== $oResponse->ResponseList[2]) { - $bResult |= $this->setStatus($oResponse->ResponseList[2], $oResponse->ResponseList[1]); + $bResult |= $this->setStatusItem($oResponse->ResponseList[2], $oResponse->ResponseList[1]); } } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php index 94e773b4c..33fb026e2 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Driver.php @@ -17,15 +17,9 @@ namespace MailSo\Log; */ abstract class Driver { - /** - * @var string - */ - protected $sDatePattern = 'H:i:s'; - - /** - * @var string - */ - protected $sName = ''; + protected string + $sDatePattern = 'H:i:s', + $sName = ''; /** * @var array @@ -41,25 +35,13 @@ abstract class Driver \LOG_DEBUG => '[DEBUG]' ]; - /** - * @var bool - */ - protected $bGuidPrefix = true; + protected bool $bGuidPrefix = true; - /** - * @var DateTimeZone - */ - protected $oTimeZone; + protected \DateTimeZone $oTimeZone; - /** - * @var bool - */ - protected $bTimePrefix = true; + protected bool $bTimePrefix = true; - /** - * @var bool - */ - protected $bTypedPrefix = true; + protected bool $bTypedPrefix = true; function __construct() { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php index 5a4a95f55..5f4ca9018 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/File.php @@ -18,19 +18,12 @@ namespace MailSo\Log\Drivers; */ class File extends \MailSo\Log\Driver { - /** - * @var string - */ - private $sLoggerFileName; + private string $sLoggerFileName; function __construct(string $sLoggerFileName) { parent::__construct(); - $this->SetLoggerFileName($sLoggerFileName); - } - public function SetLoggerFileName(string $sLoggerFileName) - { $sLogFileDir = \dirname($sLoggerFileName); if (!\is_dir($sLogFileDir)) { \mkdir($sLogFileDir, 0755, true); @@ -40,19 +33,14 @@ class File extends \MailSo\Log\Driver protected function writeImplementation($mDesc) : bool { - return $this->writeToLogFile($mDesc); + if (\is_array($mDesc)) { + $mDesc = \implode("\n\t", $mDesc); + } + return \error_log($mDesc . "\n", 3, $this->sLoggerFileName); } protected function clearImplementation() : bool { return \unlink($this->sLoggerFileName); } - - private function writeToLogFile($mDesc) : bool - { - if (\is_array($mDesc)) { - $mDesc = \implode("\n\t", $mDesc); - } - return \error_log($mDesc . "\n", 3, $this->sLoggerFileName); - } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/StderrStream.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/StderrStream.php new file mode 100644 index 000000000..89b56d886 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Drivers/StderrStream.php @@ -0,0 +1,33 @@ +oLogger; + } + + public function SetLogger(?Logger $oLogger): void + { + $this->oLogger = $oLogger; + } + + public function logWrite(string $sDesc, int $iType = \LOG_INFO, + string $sName = '', bool $bSearchSecretWords = true, bool $bDiplayCrLf = false): bool + { + return $this->oLogger && $this->oLogger->Write($sDesc, $iType, $sName, $bSearchSecretWords, $bDiplayCrLf); + } + + public function logException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = ''): void + { + $this->oLogger && $this->oLogger->WriteException($oException, $iType, $sName); + } + + public function logMask(string $sWord): void + { + $this->oLogger && $this->oLogger->AddSecret($sWord); + } + +/* + public function logWriteDump($mValue, int $iType = \LOG_INFO, string $sName = '') : bool + public function logWriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void +*/ +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php index ecec59391..f447d1f72 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Log/Logger.php @@ -17,23 +17,67 @@ namespace MailSo\Log; */ class Logger extends \SplFixedArray { - private $bUsed = false; + private bool $bUsed = false; - private $iLevel = \LOG_WARNING; + private int $iLevel = \LOG_WARNING; - private $aSecretWords = []; + private array $aSecretWords = []; - private $bShowSecrets = false; + private bool $bShowSecrets = false; + + private static $SIGNALS = [ + 'SIGABRT', + 'SIGALRM', + 'SIGBABY', + 'SIGBUS', + 'SIGCHLD', + 'SIGCLD', +// 'SIGCONT', + 'SIGFPE', + 'SIGHUP', + 'SIGILL', +// 'SIGINT', + 'SIGIO', + 'SIGIOT', +// 'SIGKILL', + 'SIGPIPE', + 'SIGPOLL', + 'SIGPROF', + 'SIGPWR', + 'SIGQUIT', + 'SIGSEGV', + 'SIGSTKFLT', +// 'SIGSTOP', + 'SIGSYS', + 'SIGTERM', + 'SIGTRAP', + 'SIGTSTP', + 'SIGTTIN', + 'SIGTTOU', + 'SIGURG', + 'SIGUSR1', + 'SIGUSR2', + 'SIGVTALRM', + 'SIGWINCH', + 'SIGXCPU', + 'SIGXFSZ' + ]; function __construct(bool $bMainLogger = false) { parent::__construct(); - if ($bMainLogger) - { - \set_error_handler(array($this, '__phpErrorHandler')); - \set_exception_handler(array($this, '__phpExceptionHandler')); - \register_shutdown_function(array($this, '__loggerShutDown')); + if ($bMainLogger) { + \set_error_handler([$this, '__phpErrorHandler']); + \set_exception_handler([$this, '__phpExceptionHandler']); + \register_shutdown_function([$this, '__loggerShutDown']); + + if (\is_callable('pcntl_signal')) { + \pcntl_async_signals(true); + foreach (static::$SIGNALS as $SIGNAL) { + \defined($SIGNAL) && \pcntl_signal(\constant($SIGNAL), [$this, 'signalHandler']); + } + } } } @@ -43,8 +87,7 @@ class Logger extends \SplFixedArray public static function Guid() : string { static $sCache = null; - if (null === $sCache) - { + if (null === $sCache) { $sCache = \substr(\MailSo\Base\Utils::Sha1Rand(), -8); } @@ -152,11 +195,28 @@ class Logger extends \SplFixedArray public function __loggerShutDown() : void { if ($this->bUsed) { + $error = \error_get_last(); + $error && $this->Write('Last error: '.\json_encode($error)); $this->Write('Memory peak usage: '.\MailSo\Base\Utils::FormatFileSize(\memory_get_peak_usage(true), 2)); $this->Write('Time delta: '.(\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'])); } } + public function signalHandler($signo) + { + if (\SIGTERM == $signo) { + exit; + } + if ($this->bUsed) { + foreach (static::$SIGNALS as $SIGNAL) { + if (\defined($SIGNAL) && \constant($SIGNAL) == $signo) { + $this->Write("Caught {$SIGNAL}"); + break; + } + } + } + } + public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bSearchSecretWords = true, bool $bDiplayCrLf = false) : bool { @@ -166,8 +226,7 @@ class Logger extends \SplFixedArray $this->bUsed = true; - if ($bSearchSecretWords && !$this->bShowSecrets && $this->aSecretWords) - { + if ($bSearchSecretWords && !$this->bShowSecrets && $this->aSecretWords) { $sDesc = \str_replace($this->aSecretWords, '*******', $sDesc); } @@ -189,19 +248,21 @@ class Logger extends \SplFixedArray return $this->Write(\print_r($mValue, true), $iType, $sName); } + private $aExceptions = []; + public function WriteException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void { - if (empty($oException->__WRITTEN__)) { - $oException->__WRITTEN__ = true; + if (!\in_array($oException, $this->aExceptions)) { $this->Write((string) $oException, $iType, $sName); + $this->aExceptions[] = $oException; } } public function WriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void { - if (empty($oException->__WRITTEN__)) { - $oException->__WRITTEN__ = true; + if (!\in_array($oException, $this->aExceptions)) { $this->Write($oException->getMessage(), $iType, $sName); + $this->aExceptions[] = $oException; } } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php index 2dd75e7eb..aa11a1c81 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Attachment.php @@ -11,30 +11,25 @@ namespace MailSo\Mail; +use MailSo\Imap\BodyStructure; + /** * @category MailSo * @package Mail */ class Attachment implements \JsonSerializable { - /** - * @var string - */ - private $sFolder; + private string $sFolder; - /** - * @var int - */ - private $iUid; + private int $iUid; - /** - * @var \MailSo\Imap\BodyStructure - */ - private $oBodyStructure; + private ?BodyStructure $oBodyStructure; - function __construct() + function __construct(string $sFolder, int $iUid, BodyStructure $oBodyStructure) { - $this->Clear(); + $this->sFolder = $sFolder; + $this->iUid = $iUid; + $this->oBodyStructure = $oBodyStructure; } public function Clear() : self @@ -56,127 +51,18 @@ class Attachment implements \JsonSerializable return $this->iUid; } - public function MimeIndex() : string + public function __call(string $name, array $arguments) //: mixed { - return $this->oBodyStructure ? $this->oBodyStructure->PartID() : ''; - } - - public function FileName(bool $bCalculateOnEmpty = false) : string - { - if (!$this->oBodyStructure) { - return ''; - } - - $sFileName = \trim($this->oBodyStructure->FileName()); - if (\strlen($sFileName) || !$bCalculateOnEmpty) { - return $sFileName; - } - - $sIdx = '-' . $this->MimeIndex(); - - $sMimeType = \strtolower(\trim($this->MimeType())); - if ('message/rfc822' === $sMimeType) { - return "message{$sIdx}.eml"; - } - if ('text/calendar' === $sMimeType) { - return "calendar{$sIdx}.ics"; - } - if ('text/plain' === $sMimeType) { - return "part{$sIdx}.txt"; - } - if (\preg_match('@text/(vcard|html|csv|xml|css|asp)@', $sMimeType, $aMatch) - || \preg_match('@image/(png|jpeg|gif|bmp|cgm|ief|tiff|webp)@', $sMimeType, $aMatch)) { - return "part{$sIdx}.{$aMatch[1]}"; - } - if (\strlen($sMimeType)) { - return \str_replace('/', $sIdx.'.', $sMimeType); - } - - return ($this->oBodyStructure->IsInline() ? 'inline' : 'part' ) . $sIdx; - } - - public function MimeType() : string - { - return $this->oBodyStructure ? $this->oBodyStructure->ContentType() : ''; - } - - public function EncodedSize() : int - { - return $this->oBodyStructure ? $this->oBodyStructure->Size() : 0; - } - - public function EstimatedSize() : int - { - return $this->oBodyStructure ? $this->oBodyStructure->EstimatedSize() : 0; - } - - public function Cid() : string - { - return $this->oBodyStructure ? $this->oBodyStructure->ContentID() : ''; - } - - public function ContentLocation() : string - { - return $this->oBodyStructure ? $this->oBodyStructure->ContentLocation() : ''; - } - - public function IsInline() : bool - { - return $this->oBodyStructure ? $this->oBodyStructure->IsInline() : false; - } - - public function IsImage() : bool - { - return $this->oBodyStructure ? $this->oBodyStructure->IsImage() : false; - } - - public function IsArchive() : bool - { - return $this->oBodyStructure ? $this->oBodyStructure->IsArchive() : false; - } - - public function IsPdf() : bool - { - return $this->oBodyStructure ? $this->oBodyStructure->IsPdf() : false; - } - - public function IsDoc() : bool - { - return $this->oBodyStructure ? $this->oBodyStructure->IsDoc() : false; - } - - public function IsPgpSignature() : bool - { - return $this->oBodyStructure ? $this->oBodyStructure->IsPgpSignature() : false; - } - - public static function NewBodyStructureInstance(string $sFolder, int $iUid, \MailSo\Imap\BodyStructure $oBodyStructure) : self - { - return (new self)->InitByBodyStructure($sFolder, $iUid, $oBodyStructure); - } - - public function InitByBodyStructure(string $sFolder, int $iUid, \MailSo\Imap\BodyStructure $oBodyStructure) : self - { - $this->sFolder = $sFolder; - $this->iUid = $iUid; - $this->oBodyStructure = $oBodyStructure; - return $this; + return $this->oBodyStructure->{$name}(...$arguments); } #[\ReturnTypeWillChange] public function jsonSerialize() { - return array( + return \array_merge([ '@Object' => 'Object/Attachment', - 'Folder' => $this->Folder(), - 'Uid' => (string) $this->Uid(), - 'MimeIndex' => (string) $this->MimeIndex(), - 'MimeType' => $this->MimeType(), - 'FileName' => \MailSo\Base\Utils::SecureFileName($this->FileName(true)), - 'EstimatedSize' => $this->EstimatedSize(), - 'Cid' => $this->Cid(), - 'ContentLocation' => $this->ContentLocation(), - 'IsInline' => $this->IsInline() - ); + 'folder' => $this->sFolder, + 'uid' => $this->iUid + ], $this->oBodyStructure->jsonSerialize()); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php deleted file mode 100644 index 5e4a66dc4..000000000 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Folder.php +++ /dev/null @@ -1,240 +0,0 @@ -oImapFolder = $oImapFolder; - $this->bSubscribed = $bSubscribed || \in_array('\\subscribed', $oImapFolder->FlagsLowerCase()); - $this->bExists = $bExists; - } - - /** - * @throws \InvalidArgumentException - * @throws \InvalidArgumentException - */ - public static function NewNonExistentInstance(string $sFullName, string $sDelimiter) : self - { - return new self( - new \MailSo\Imap\Folder($sFullName, $sDelimiter, array('\\Noselect')), false, false); - } - - public function Name() : string - { - return $this->oImapFolder->Name(); - } - - public function FullName() : string - { - return $this->oImapFolder->FullName(); - } - - public function NameRaw() : string - { - return $this->oImapFolder->NameRaw(); - } - - public function Delimiter() : string - { - return $this->oImapFolder->Delimiter(); - } - - public function FlagsLowerCase() : array - { - return $this->oImapFolder->FlagsLowerCase(); - } - - public function IsSubscribed() : bool - { - return $this->bSubscribed; - } - - public function IsExists() : bool - { - return $this->bExists; - } - - public function IsSelectable() : bool - { - return $this->bExists && $this->oImapFolder->IsSelectable(); - } - - public function Status() : array - { - return $this->oImapFolder->getStatusItems(); - } - - public function IsInbox() : bool - { - return $this->oImapFolder->IsInbox(); - } - - public function GetFolderListType() : int - { - $aFlags = $this->oImapFolder->FlagsLowerCase(); - // RFC 6154 -// $aFlags[] = \strtolower($this->oImapFolder->GetMetadata(MetadataKeys::SPECIALUSE)); - - switch (true) - { - case \in_array('\\inbox', $aFlags) || 'INBOX' === \strtoupper($this->FullName()): - return FolderType::INBOX; - - case \in_array('\\sent', $aFlags): - case \in_array('\\sentmail', $aFlags): - return FolderType::SENT; - - case \in_array('\\drafts', $aFlags): - return FolderType::DRAFTS; - - case \in_array('\\junk', $aFlags): - case \in_array('\\spam', $aFlags): - return FolderType::JUNK; - - case \in_array('\\trash', $aFlags): - case \in_array('\\bin', $aFlags): - return FolderType::TRASH; - - case \in_array('\\important', $aFlags): - return FolderType::IMPORTANT; - - case \in_array('\\flagged', $aFlags): - case \in_array('\\starred', $aFlags): - return FolderType::FLAGGED; - - case \in_array('\\archive', $aFlags): - return FolderType::ARCHIVE; - - case \in_array('\\all', $aFlags): - case \in_array('\\allmail', $aFlags): - return FolderType::ALL; - - // TODO -// case 'Templates' === $this->FullName(): -// return FolderType::TEMPLATES; - } - - // Kolab - $type = $this->GetMetadata(MetadataKeys::KOLAB_CTYPE) ?: $this->GetMetadata(MetadataKeys::KOLAB_CTYPE_SHARED); - switch ($type) - { -/* - // TODO: Kolab - case 'event': - case 'event.default': - return FolderType::CALENDAR; - case 'contact': - case 'contact.default': - return FolderType::CONTACTS; - case 'task': - case 'task.default': - return FolderType::TASKS; - case 'note': - case 'note.default': - return FolderType::NOTES; - case 'file': - case 'file.default': - return FolderType::FILES; - case 'configuration': - return FolderType::CONFIGURATION; - case 'journal': - case 'journal.default': - return FolderType::JOURNAL; -*/ - case 'mail.inbox': - return FolderType::INBOX; -// case 'mail.outbox': - case 'mail.sentitems': - return FolderType::SENT; - case 'mail.drafts': - return FolderType::DRAFTS; - case 'mail.junkemail': - return FolderType::JUNK; - case 'mail.wastebasket': - return FolderType::TRASH; - } - - return FolderType::USER; - } - - /** - * @return mixed - */ - public function GetMetadata(string $sName) : ?string - { - return $this->oImapFolder->GetMetadata($sName); - } - - #[\ReturnTypeWillChange] - public function jsonSerialize() - { -/* - $aExtended = null; - $aStatus = $this->oImapFolder->getStatusItems(); - if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UNSEEN'], $aStatus['UIDNEXT'])) { - $aExtended = array( - 'totalEmails' => (int) $aStatus['MESSAGES'], - 'unreadEmails' => (int) $aStatus['UNSEEN'], - 'UidNext' => (int) $aStatus['UIDNEXT'], -// 'Hash' => $this->MailClient()->GenerateFolderHash( -// $this->FullName(), $aStatus['MESSAGES'], $aStatus['UIDNEXT'], -// empty($aStatus['HIGHESTMODSEQ']) ? 0 : $aStatus['HIGHESTMODSEQ']) - ); - } -*/ - return array( - '@Object' => 'Object/Folder', - 'Name' => $this->Name(), - 'FullName' => $this->FullName(), - 'Delimiter' => (string) $this->Delimiter(), - 'isSubscribed' => $this->bSubscribed, - 'Exists' => $this->bExists, - 'Selectable' => $this->IsSelectable(), - 'Flags' => $this->FlagsLowerCase(), -// 'Extended' => $aExtended, -// 'PermanentFlags' => $this->oImapFolder->PermanentFlags, - 'Metadata' => $this->oImapFolder->Metadata(), - 'UidNext' => $this->oImapFolder->UIDNEXT, - // RFC 8621 - 'totalEmails' => $this->oImapFolder->MESSAGES, - 'unreadEmails' => $this->oImapFolder->UNSEEN, - 'id' => $this->oImapFolder->MAILBOXID - ); - } -} 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 dc8e191e0..ca5d4a67e 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 @@ -11,11 +11,12 @@ namespace MailSo\Mail; -use MailSo\Imap\Enumerations\FolderResponseStatus; +use MailSo\Imap\FolderCollection; +use MailSo\Imap\FolderInformation; +use MailSo\Imap\Enumerations\FetchType; use MailSo\Imap\Enumerations\MessageFlag; use MailSo\Imap\Enumerations\StoreAction; use MailSo\Imap\SequenceSet; -use MailSo\Imap\Enumerations\FetchType; use MailSo\Mime\Enumerations\Header as MimeHeader; use MailSo\Mime\Enumerations\Parameter as MimeParameter; @@ -25,20 +26,13 @@ use MailSo\Mime\Enumerations\Parameter as MimeParameter; */ class MailClient { - /** - * @var \MailSo\Log\Logger - */ - private $oLogger = null; + use \MailSo\Log\Inherit; - /** - * @var \MailSo\Imap\ImapClient - */ - private $oImapClient; + private \MailSo\Imap\ImapClient $oImapClient; function __construct() { $this->oImapClient = new \MailSo\Imap\ImapClient; - $this->oImapClient->SetTimeOuts(10, \MailSo\Config::$ImapTimeout); } public function ImapClient() : \MailSo\Imap\ImapClient @@ -48,8 +42,7 @@ class MailClient private function getEnvelopeOrHeadersRequestString() : string { - if (\MailSo\Config::$MessageAllHeaders) - { + if ($this->oImapClient->Settings->message_all_headers) { return FetchType::BODY_HEADER_PEEK; } @@ -121,10 +114,9 @@ class MailClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null, int $iBodyTextLimit = 0) : ?Message + public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null) : ?Message { - if (!\MailSo\Base\Validator::RangeInt($iIndex, 1)) - { + if (1 > $iIndex) { throw new \InvalidArgumentException; } @@ -143,19 +135,15 @@ class MailClient ); $aFetchResponse = $this->oImapClient->Fetch(array(FetchType::BODYSTRUCTURE), $iIndex, $bIndexIsUid); - if (\count($aFetchResponse) && isset($aFetchResponse[0])) - { + if (\count($aFetchResponse) && isset($aFetchResponse[0])) { $oBodyStructure = $aFetchResponse[0]->GetFetchBodyStructure(); - if ($oBodyStructure) - { - foreach ($oBodyStructure->GetHtmlAndPlainParts() as $oPart) - { + if ($oBodyStructure) { + $iBodyTextLimit = $this->oImapClient->Settings->body_text_limit; + foreach ($oBodyStructure->GetHtmlAndPlainParts() as $oPart) { $sLine = FetchType::BODY_PEEK.'['.$oPart->PartID().']'; - if (0 < $iBodyTextLimit && $iBodyTextLimit < $oPart->Size()) - { + if (0 < $iBodyTextLimit && $iBodyTextLimit < $oPart->EstimatedSize()) { $sLine .= "<0.{$iBodyTextLimit}>"; } - $aFetchItems[] = $sLine; } /* @@ -173,15 +161,13 @@ class MailClient } } - if (!$oBodyStructure) - { + if (!$oBodyStructure) { $aFetchItems[] = FetchType::BODYSTRUCTURE; } $aFetchResponse = $this->oImapClient->Fetch($aFetchItems, $iIndex, $bIndexIsUid); - if (\count($aFetchResponse)) - { - $oMessage = Message::NewFetchResponseInstance($sFolderName, $aFetchResponse[0], $oBodyStructure); + if (\count($aFetchResponse)) { + $oMessage = Message::fromFetchResponse($sFolderName, $aFetchResponse[0], $oBodyStructure); } return $oMessage; @@ -199,8 +185,7 @@ class MailClient */ public function MessageMimeStream($mCallback, string $sFolderName, int $iIndex, string $sMimeIndex) : bool { - if (!\is_callable($mCallback)) - { + if (!\is_callable($mCallback)) { throw new \InvalidArgumentException; } @@ -218,20 +203,17 @@ class MailClient : FetchType::BODY_HEADER_PEEK), $iIndex, true); - if (\count($aFetchResponse)) - { + if (\count($aFetchResponse)) { $sMime = $aFetchResponse[0]->GetFetchValue( \strlen($sMimeIndex) ? FetchType::BODY.'['.$sMimeIndex.'.MIME]' : FetchType::BODY_HEADER ); - if (\strlen($sMime)) - { + if (\strlen($sMime)) { $oHeaders = new \MailSo\Mime\HeaderCollection($sMime); - if (\strlen($sMimeIndex)) - { + if (\strlen($sMimeIndex)) { $sFileName = $oHeaders->ParameterValue(MimeHeader::CONTENT_DISPOSITION, MimeParameter::FILENAME); if (!\strlen($sFileName)) { $sFileName = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::NAME); @@ -243,15 +225,13 @@ class MailClient // RFC 3516 // Should mailserver decode or PHP? - if ($sMailEncoding && $this->oImapClient->IsSupported('BINARY')) { + if ($sMailEncoding && $this->oImapClient->hasCapability('BINARY')) { $sMailEncoding = ''; $sPeek = FetchType::BINARY_PEEK; } $sContentType = $oHeaders->ValueByName(MimeHeader::CONTENT_TYPE); - } - else - { + } else { $sFileName = ($oHeaders->ValueByName(MimeHeader::SUBJECT) ?: $iIndex) . '.eml'; $sContentType = 'message/rfc822'; @@ -259,187 +239,55 @@ class MailClient } } - $aFetchResponse = $this->oImapClient->Fetch(array( -// FetchType::BINARY_SIZE.'['.$sMimeIndex.']', - // Push in the aFetchCallbacks array and then called by \MailSo\Imap\Traits\ResponseParser::partialResponseLiteralCallbackCallable - array( - $sPeek.'['.$sMimeIndex.']', - function ($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream) use ($mCallback, $sMimeIndex, $sMailEncoding, $sContentType, $sFileName) - { - if (\strlen($sLiteralAtomUpperCase) && \is_resource($rImapLiteralStream) && 'FETCH' === $sParent) - { - $mCallback($sMailEncoding - ? \MailSo\Base\StreamWrappers\Binary::CreateStream($rImapLiteralStream, $sMailEncoding) - : $rImapLiteralStream, - $sContentType, $sFileName, $sMimeIndex); - } + $callback = function ($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream) + use ($mCallback, $sMimeIndex, $sMailEncoding, $sContentType, $sFileName) + { + if (\strlen($sLiteralAtomUpperCase) && \is_resource($rImapLiteralStream) && 'FETCH' === $sParent) { + $mCallback($sMailEncoding + ? \MailSo\Base\StreamWrappers\Binary::CreateStream($rImapLiteralStream, $sMailEncoding) + : $rImapLiteralStream, + $sContentType, $sFileName, $sMimeIndex); } - )), $iIndex, true); + }; + + try { + $aFetchResponse = $this->oImapClient->Fetch(array( +// FetchType::BINARY_SIZE.'['.$sMimeIndex.']', + // Push in the aFetchCallbacks array and then called by \MailSo\Imap\Traits\ResponseParser::partialResponseLiteralCallbackCallable + array( + $sPeek.'['.$sMimeIndex.']', + $callback + )), $iIndex, true); + } catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException) { + if (FetchType::BINARY_PEEK === $sPeek && \preg_match('/UNKNOWN-CTE|PARSE/', $oException->getMessage())) { + $this->logException($oException, \LOG_WARNING); + $aFetchResponse = $this->oImapClient->Fetch(array( + array( + FetchType::BODY_PEEK . '[' . $sMimeIndex . ']', + $callback + )), $iIndex, true); + } else { + throw $e; + } + } return ($aFetchResponse && 1 === \count($aFetchResponse)); } - /** - * @throws \InvalidArgumentException - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - * @throws \MailSo\Imap\Exceptions\* - */ - public function MessageDelete(string $sFolder, SequenceSet $oRange, bool $bExpungeAll = false) : self + public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, array $aAppendFlags = null) : int { - if (!\strlen($sFolder) || !\count($oRange)) - { - throw new \InvalidArgumentException; - } - - $this->oImapClient->FolderSelect($sFolder); - - $this->oImapClient->MessageStoreFlag($oRange, - array(MessageFlag::DELETED), - StoreAction::ADD_FLAGS_SILENT - ); - - if ($bExpungeAll) { - $this->oImapClient->FolderExpunge(); - } else { - $this->oImapClient->FolderExpunge($oRange); - } - - return $this; - } - - /** - * @throws \InvalidArgumentException - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - * @throws \MailSo\Imap\Exceptions\* - */ - public function MessageMove(string $sFromFolder, string $sToFolder, SequenceSet $oRange, bool $bUseMoveSupported = true, bool $bExpungeAll = false) : self - { - if (!$sFromFolder || !$sToFolder || !\count($oRange)) - { - throw new \InvalidArgumentException; - } - - $this->oImapClient->FolderSelect($sFromFolder); - - if ($bUseMoveSupported && $this->oImapClient->IsSupported('MOVE')) - { - $this->oImapClient->MessageMove($sToFolder, $oRange); - } - else - { - $this->oImapClient->MessageCopy($sToFolder, $oRange); - - $this->MessageDelete($sFromFolder, $oRange, $bExpungeAll); - } - - return $this; - } - - /** - * @throws \InvalidArgumentException - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - * @throws \MailSo\Imap\Exceptions\* - */ - public function MessageCopy(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : self - { - if (!$sFromFolder || !$sToFolder || !\count($oRange)) - { - throw new \InvalidArgumentException; - } - - $this->oImapClient->FolderSelect($sFromFolder); - $this->oImapClient->MessageCopy($sToFolder, $oRange); - - return $this; - } - - /** - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - * @throws \MailSo\Imap\Exceptions\* - */ - public function FolderUnselect() : self - { - if ($this->oImapClient->IsSelected()) - { - $this->oImapClient->FolderUnselect(); - } - - return $this; - } - - /** - * @param resource $rMessageStream - */ - public function MessageAppendStream($rMessageStream, int $iMessageStreamSize, string $sFolderToSave, array $aAppendFlags = null, int &$iUid = null) : self - { - if (!\is_resource($rMessageStream) || !\strlen($sFolderToSave)) - { - throw new \InvalidArgumentException; - } - - $iUid = $this->oImapClient->MessageAppendStream( - $sFolderToSave, $rMessageStream, $iMessageStreamSize, $aAppendFlags); - - return $this; - } - - public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, array $aAppendFlags = null, int &$iUid = null) : self - { - if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) - { + if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) { throw new \InvalidArgumentException; } $iMessageStreamSize = \filesize($sMessageFileName); $rMessageStream = \fopen($sMessageFileName, 'rb'); - $this->MessageAppendStream($rMessageStream, $iMessageStreamSize, $sFolderToSave, $aAppendFlags, $iUid); + $iUid = $this->oImapClient->MessageAppendStream($sFolderToSave, $rMessageStream, $iMessageStreamSize, $aAppendFlags); - if (\is_resource($rMessageStream)) - { - fclose($rMessageStream); - } + \fclose($rMessageStream); - return $this; - } - - protected function initFolderValues(string $sFolderName) : array - { - $oFolderStatus = $this->oImapClient->FolderStatus($sFolderName); - return [ - \max(0, $oFolderStatus->MESSAGES ?: 0), - - \max(0, $oFolderStatus->UNSEEN ?: 0), - - \max(0, $oFolderStatus->UIDNEXT ?: 0), - - \max(0, $oFolderStatus->HIGHESTMODSEQ ?: 0), - - $oFolderStatus->APPENDLIMIT ?: $this->oImapClient->AppendLimit(), - - $oFolderStatus->MAILBOXID ?: '' - ]; - } - - public function GenerateImapClientHash() : string - { - return \md5('ImapClientHash/'. - $this->oImapClient->GetLogginedUser().'@'. - $this->oImapClient->GetConnectedHost().':'. - $this->oImapClient->GetConnectedPort() - ); - } - - public function GenerateFolderHash(string $sFolder, int $iCount, int $iUidNext, int $iHighestModSeq) : string - { - return \md5('FolderHash/'.$sFolder.'-'.$iCount.'-'.$iUidNext.'-'. - $iHighestModSeq.'-'.$this->GenerateImapClientHash().'-'. - \MailSo\Config::$MessageListPermanentFilter - ); + return $iUid; } /** @@ -450,8 +298,7 @@ class MailClient { $aNewMessages = array(); - if ($iPrevUidNext && $iPrevUidNext != $iCurrentUidNext && 'INBOX' === $sFolderName && \MailSo\Config::$CheckNewMessages) - { + if ($this->oImapClient->Settings->fetch_new_messages && $iPrevUidNext && $iPrevUidNext != $iCurrentUidNext && 'INBOX' === $sFolderName) { $this->oImapClient->FolderExamine($sFolderName); $aFetchResponse = $this->oImapClient->Fetch(array( @@ -464,12 +311,10 @@ class MailClient )) ), $iPrevUidNext.':*', true); - foreach ($aFetchResponse as /* @var $oFetchResponse \MailSo\Imap\FetchResponse */ $oFetchResponse) - { + foreach ($aFetchResponse as /* @var $oFetchResponse \MailSo\Imap\FetchResponse */ $oFetchResponse) { $aFlags = \array_map('strtolower', $oFetchResponse->GetFetchValue(FetchType::FLAGS)); - if (!\in_array(\strtolower(MessageFlag::SEEN), $aFlags)) - { + if (!\in_array(\strtolower(MessageFlag::SEEN), $aFlags)) { $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); $oHeaders = new \MailSo\Mime\HeaderCollection($oFetchResponse->GetHeaderFieldsValue()); @@ -481,10 +326,10 @@ class MailClient } $aNewMessages[] = array( - 'Folder' => $sFolderName, - 'Uid' => $iUid, + 'folder' => $sFolderName, + 'uid' => $iUid, 'subject' => $oHeaders->ValueByName(MimeHeader::SUBJECT, !$sContentTypeCharset), - 'From' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, !$sContentTypeCharset) + 'from' => $oHeaders->GetAsEmailCollection(MimeHeader::FROM_, !$sContentTypeCharset) ); } } @@ -501,47 +346,38 @@ class MailClient */ public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array { - list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq, $iAppendLimit, $sMailboxId) = $this->initFolderValues($sFolderName); -/* - // Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot - $oInfo = $this->oImapClient->FolderSelect($sFolderName); - $oInfo->UNSEEN = $iUnseenCount; - $oInfo->HIGHESTMODSEQ = $iHighestModSeq; - $oInfo->Hash = $this->GenerateFolderHash($oInfo->FolderName, $oInfo->MESSAGES, $oInfo->UIDNEXT, $oInfo->HIGHESTMODSEQ); -*/ - $aFlags = array(); - if ($oRange && \count($oRange)) { - $oInfo = $this->oImapClient->FolderExamine($sFolderName); - - $aFetchResponse = $this->oImapClient->Fetch(array( - FetchType::UID, - FetchType::FLAGS - ), (string) $oRange, $oRange->UID); - - foreach ($aFetchResponse as $oFetchResponse) { - $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); - $aLowerFlags = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oFetchResponse->GetFetchValue(FetchType::FLAGS))); - $aFlags[] = array( - 'Uid' => $iUid, - 'Flags' => $aLowerFlags - ); + if ($oRange) { +// $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize(); + $aInfo = $this->oImapClient->FolderStatusAndSelect($sFolderName)->jsonSerialize(); + $aInfo['messagesFlags'] = array(); + if (\count($oRange)) { + $aFetchResponse = $this->oImapClient->Fetch(array( + FetchType::UID, + FetchType::FLAGS + ), (string) $oRange, $oRange->UID); + foreach ($aFetchResponse as $oFetchResponse) { + $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); + $aLowerFlags = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oFetchResponse->GetFetchValue(FetchType::FLAGS))); + $aInfo['messagesFlags'][] = array( + 'uid' => $iUid, + 'flags' => $aLowerFlags + ); + } } + } else { + $aInfo = $this->oImapClient->FolderStatus($sFolderName)->jsonSerialize(); } - return array( - 'Folder' => $sFolderName, - 'totalEmails' => $iCount, - 'unreadEmails' => $iUnseenCount, - 'UidNext' => $iUidNext, - 'HighestModSeq' => $iHighestModSeq, - 'AppendLimit' => $iAppendLimit, - 'MailboxId' => $sMailboxId, -// 'Flags' => $oInfo->Flags, -// 'PermanentFlags' => $oInfo->PermanentFlags, - 'Hash' => $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq), - 'MessagesFlags' => $aFlags, - 'NewMessages' => $this->getFolderNextMessageInformation($sFolderName, $iPrevUidNext, $iUidNext) - ); + if ($iPrevUidNext) { + $aInfo['newMessages'] = $this->getFolderNextMessageInformation( + $sFolderName, + $iPrevUidNext, + \intval($aInfo['uidNext']) + ); + } + +// $aInfo['appendLimit'] = $aInfo['appendLimit'] ?: $this->oImapClient->AppendLimit(); + return $aInfo; } /** @@ -552,9 +388,8 @@ class MailClient */ public function FolderHash(string $sFolderName) : string { - list($iCount, $iUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($sFolderName); - - return $this->GenerateFolderHash($sFolderName, $iCount, $iUidNext, $iHighestModSeq); + return $this->oImapClient->FolderStatus($sFolderName)->etag; +// return $this->oImapClient->FolderStatusAndSelect($sFolderName)->etag; } /** @@ -563,43 +398,31 @@ class MailClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - public function MessageListThreadsMap(string $sFolderName, string $sFolderHash, ?\MailSo\Cache\CacheClient $oCacher) : array + protected function MessageListThreadsMap(MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher) : array { - $sSearchHash = ''; - if (0 < \MailSo\Config::$MessageListDateFilter) - { - $iD = \time() - 3600 * 24 * 30 * \MailSo\Config::$MessageListDateFilter; - $iTimeFilter = \gmmktime(1, 1, 1, \gmdate('n', $iD), 1, \gmdate('Y', $iD)); + $sFolderName = $oMessageCollection->FolderName; - $sSearchHash .= ' SINCE '.\gmdate('j-M-Y', $iTimeFilter); + $sSearch = 'ALL'; +/* + $iThreadLimit = $this->oImapClient->Settings->thread_limit; + if ($iThreadLimit && $iThreadLimit < $oMessageCollection->FolderInfo->MESSAGES) { + $sSearch = ($oMessageCollection->FolderInfo->MESSAGES - $iThreadLimit) . ':*'; } +*/ - if ('' === \trim($sSearchHash)) - { - $sSearchHash = 'ALL'; - } - - if ($oCacher && $oCacher->IsInited()) - { + $sSerializedHashKey = null; + if ($oCacher && $oCacher->IsInited()) { $sSerializedHashKey = - 'ThreadsMapSorted/'.$sSearchHash.'/'.$sFolderName.'/'.$sFolderHash; + "ThreadsMapSorted/{$sSearch}/{$sFolderName}/{$oMessageCollection->FolderInfo->etag}"; +// "ThreadsMapSorted/{$sSearch}/{$iThreadLimit}/{$sFolderName}/{$oMessageCollection->FolderInfo->etag}"; - if ($this->oLogger) - { - $this->oLogger->Write($sSerializedHashKey); - } + $this->logWrite($sSerializedHashKey); $sSerializedUids = $oCacher->Get($sSerializedHashKey); - if (!empty($sSerializedUids)) - { + if (!empty($sSerializedUids)) { $aSerializedUids = \json_decode($sSerializedUids, true); - if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) - { - if ($this->oLogger) - { - $this->oLogger->Write('Get Serialized Thread UIDS from cache ("'.$sFolderName.'" / '.$sSearchHash.') [count:'.\count($aSerializedUids['ThreadsUids']).']'); - } - + if (isset($aSerializedUids['ThreadsUids']) && \is_array($aSerializedUids['ThreadsUids'])) { + $this->logWrite('Get Serialized Thread UIDS from cache ("'.$sFolderName.'" / '.$sSearch.') [count:'.\count($aSerializedUids['ThreadsUids']).']'); return $aSerializedUids['ThreadsUids']; } } @@ -610,7 +433,7 @@ class MailClient $aResult = array(); try { - foreach ($this->oImapClient->MessageSimpleThread($sSearchHash) as $mItem) { + foreach ($this->oImapClient->MessageSimpleThread($sSearch) as $mItem) { // Flatten to single level $aMap = []; \array_walk_recursive($mItem, function($a) use (&$aMap) { $aMap[] = $a; }); @@ -619,20 +442,16 @@ class MailClient } catch (\MailSo\RuntimeException $oException) { - \SnappyMail\Log::warning('MessageListThreadsMap ' . $oException->getMessage()); + \SnappyMail\Log::warning('MailClient', 'MessageListThreadsMap ' . $oException->getMessage()); unset($oException); } - if ($oCacher && $oCacher->IsInited() && !empty($sSerializedHashKey)) - { + if (!empty($sSerializedHashKey)) { $oCacher->Set($sSerializedHashKey, \json_encode(array( 'ThreadsUids' => $aResult ))); - if ($this->oLogger) - { - $this->oLogger->Write('Save Serialized Thread UIDS to cache ("'.$sFolderName.'" / '.$sSearchHash.') [count:'.\count($aResult).']'); - } + $this->logWrite('Save Serialized Thread UIDS to cache ("'.$sFolderName.'" / '.$sSearch.') [count:'.\count($aResult).']'); } return $aResult; @@ -643,11 +462,11 @@ class MailClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Imap\Exceptions\* */ - protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange) : void + protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange, + array &$aAllThreads = [], array &$aUnseenUIDs = []) : void { - if (\count($oRange)) - { - $aFetchResponse = $this->oImapClient->Fetch(array( + if (\count($oRange)) { + $aFetchIterator = $this->oImapClient->FetchIterate(array( FetchType::UID, FetchType::RFC822_SIZE, FetchType::INTERNALDATE, @@ -655,32 +474,33 @@ class MailClient FetchType::BODYSTRUCTURE, $this->getEnvelopeOrHeadersRequestString() ), (string) $oRange, $oRange->UID); - - if (\count($aFetchResponse)) - { - $aCollection = \array_fill_keys($oRange->getArrayCopy(), null); - foreach ($aFetchResponse as /* @var $oFetchResponseItem \MailSo\Imap\FetchResponse */ $oFetchResponseItem) { - $id = $oRange->UID - ? $oFetchResponseItem->GetFetchValue(FetchType::UID) - : $this->oImapResponse->ResponseList[1]; - $aCollection[$id] = Message::NewFetchResponseInstance($oMessageCollection->FolderName, $oFetchResponseItem); + // FETCH does not respond in the id order of the SequenceSet, so we prefill $aCollection for the right sort order. + $aCollection = \array_fill_keys($oRange->getArrayCopy(), null); + foreach ($aFetchIterator as /* @var $oFetchResponseItem \MailSo\Imap\FetchResponse */ $oFetchResponseItem) { + $id = $oRange->UID + ? $oFetchResponseItem->GetFetchValue(FetchType::UID) + : $oFetchResponseItem->oImapResponse->ResponseList[1]; + $oMessage = Message::fromFetchResponse($oMessageCollection->FolderName, $oFetchResponseItem); + if ($oMessage) { + if ($aAllThreads) { + $iUid = $oMessage->Uid; + // Find thread and set it. + // Used by GUI to delete/move the whole thread or other features + foreach ($aAllThreads as $aMap) { + if (\in_array($iUid, $aMap)) { + $oMessage->SetThreads($aMap); + $oMessage->SetThreadUnseen(\array_values(\array_intersect($aUnseenUIDs, $aMap))); + break; + } + } + } + $aCollection[$id] = $oMessage; } - $oMessageCollection->exchangeArray(\array_values(\array_filter($aCollection))); } + $oMessageCollection->exchangeArray(\array_values(\array_filter($aCollection))); } } - /** - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - */ - public function IsThreadsSupported() : bool - { - return $this->oImapClient->IsSupported('THREAD=REFS') || - $this->oImapClient->IsSupported('THREAD=REFERENCES') || - $this->oImapClient->IsSupported('THREAD=ORDEREDSUBJECT'); - } - /** * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException @@ -688,10 +508,13 @@ class MailClient * @throws \MailSo\Imap\Exceptions\* */ private function GetUids(MessageListParams $oParams, string $sSearch, - string $sFolderName, string $sFolderHash, - bool $bUseSortIfSupported = false, string $sSort = '') : array + FolderInformation $oInfo, bool $bUseSort = false) : array { $oCacher = $oParams->oCacher; + $sFolderName = $oParams->sFolderName; + + $bUseSort = $bUseSort && $this->oImapClient->hasCapability('SORT'); + $sSort = $bUseSort ? $oParams->sSort : ''; /* TODO: Validate $sSort ARRIVAL Internal date and time of the message. This differs from the @@ -729,70 +552,91 @@ class MailClient [IMAP] addr-mailbox of the first "To" address. RFC 5957: - $this->oImapClient->IsSupported('SORT=DISPLAY') + $this->oImapClient->hasCapability('SORT=DISPLAY') DISPLAYFROM, DISPLAYTO */ - $aResultUids = false; - $bUidsFromCacher = false; $bUseCacheAfterSearch = $oCacher && $oCacher->IsInited(); + $sSearchCriterias = \MailSo\Imap\SearchCriterias::fromString($this->oImapClient, $sFolderName, $sSearch, $oParams->bHideDeleted, $bUseCacheAfterSearch); + // Disable? as there are many cases that change the result +// $bUseCacheAfterSearch = false; + + $bReturnUid = true; + if ($oParams->oSequenceSet) { + $bReturnUid = $oParams->oSequenceSet->UID; + $sSearchCriterias = $oParams->oSequenceSet . ' ' . $sSearchCriterias; + } $sSerializedHash = ''; $sSerializedLog = ''; - - $bUseSortIfSupported = $bUseSortIfSupported && !\strlen($sSearch) && $this->oImapClient->IsSupported('SORT'); - - $sSearchCriterias = \MailSo\Imap\SearchCriterias::fromString($this->oImapClient, $sFolderName, $sSearch, $oParams->bHideDeleted, $bUseCacheAfterSearch); - // Disabled for now as there are many cases that change the result - $bUseCacheAfterSearch = false; if ($bUseCacheAfterSearch) { - $sSerializedHash = 'GetUids/'. - ($bUseSortIfSupported ? 'S' . $sSort : 'N').'/'. - $this->GenerateImapClientHash().'/'. - $sFolderName.'/'.$sSearchCriterias; - $sSerializedLog = '"'.$sFolderName.'" / '.$sSearchCriterias.''; + $sSerializedHash = 'Get' + . ($bReturnUid ? 'UIDS/' : 'IDS/') + . ($bUseSort ? 'S' . $sSort : 'N') + . "/{$this->oImapClient->Hash()}/{$sFolderName}/{$sSearchCriterias}"; + $sSerializedLog = "\"{$sFolderName}\" / {$sSort} / {$sSearchCriterias}"; $sSerialized = $oCacher->Get($sSerializedHash); if (!empty($sSerialized)) { $aSerialized = \json_decode($sSerialized, true); if (\is_array($aSerialized) && isset($aSerialized['FolderHash'], $aSerialized['Uids']) && - $sFolderHash === $aSerialized['FolderHash'] && + $oInfo->etag === $aSerialized['FolderHash'] && \is_array($aSerialized['Uids']) ) { - if ($this->oLogger) { - $this->oLogger->Write('Get Serialized UIDS from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']'); - } - - $aResultUids = $aSerialized['Uids']; - $bUidsFromCacher = true; + $this->logWrite('Get Serialized '.($bReturnUid?'UIDS':'IDS').' from cache ('.$sSerializedLog.') [count:'.\count($aSerialized['Uids']).']'); + return $aSerialized['Uids']; } } } - if (!$bUidsFromCacher) { - if ($bUseSortIfSupported) { -// $this->oImapClient->IsSupported('ESORT') -// $aResultUids = $this->oImapClient->MessageSimpleESort(array($sSort ?: 'REVERSE DATE'), $sSearchCriterias)['ALL']; - $aResultUids = $this->oImapClient->MessageSimpleSort(array($sSort ?: 'REVERSE DATE'), $sSearchCriterias); - } else { -// $this->oImapClient->IsSupported('ESEARCH') -// $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, true, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8') - $aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, true, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8'); - } + $this->oImapClient->FolderExamine($sFolderName); - if ($bUseCacheAfterSearch) { - $oCacher->Set($sSerializedHash, \json_encode(array( - 'FolderHash' => $sFolderHash, - 'Uids' => $aResultUids - ))); - - if ($this->oLogger) { - $this->oLogger->Write('Save Serialized UIDS to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']'); - } + $aResultUids = []; + if ($bUseSort) { + $aSortTypes = []; + if ($sSort) { + $aSortTypes[] = $sSort; } + if (false === \strpos($sSort, 'DATE')) { + // Always also sort DATE descending when DATE is not defined + $aSortTypes[] = 'REVERSE DATE'; + } +// $this->oImapClient->hasCapability('ESORT') +// $aResultUids = $this->oImapClient->MessageSimpleESort($aSortTypes, $sSearchCriterias)['ALL']; + $aResultUids = $this->oImapClient->MessageSimpleSort($aSortTypes, $sSearchCriterias, $bReturnUid); + } else { +// $this->oImapClient->hasCapability('ESEARCH') +// $aResultUids = $this->oImapClient->MessageSimpleESearch($sSearchCriterias, null, $bReturnUid, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8') + $aResultUids = $this->oImapClient->MessageSimpleSearch($sSearchCriterias, $bReturnUid, \MailSo\Base\Utils::IsAscii($sSearchCriterias) ? '' : 'UTF-8'); } - return \is_array($aResultUids) ? $aResultUids : array(); + if ($bUseCacheAfterSearch) { + $oCacher->Set($sSerializedHash, \json_encode(array( + 'FolderHash' => $oInfo->etag, + 'Uids' => $aResultUids + ))); + + $this->logWrite('Save Serialized '.($bReturnUid?'UIDS':'IDS').' to cache ('.$sSerializedLog.') [count:'.\count($aResultUids).']'); + } + + return $aResultUids; + } + + public function MessageListUnseen(MessageListParams $oParams, FolderInformation $oInfo) : array + { + $oUnseenParams = new MessageListParams; + $oUnseenParams->sFolderName = $oParams->sFolderName; +// $oUnseenParams->sSearch = $oParams->sSearch; +// $oUnseenParams->sSort = $oParams->sSort; + $oUnseenParams->oCacher = $oParams->oCacher; + $oUnseenParams->bUseSort = false; // $oParams->bUseSort + $oUnseenParams->bUseThreads = false; // $oParams->bUseThreads; + $oUnseenParams->bHideDeleted = $oParams->bHideDeleted; +// $oUnseenParams->iOffset = $oParams->iOffset; +// $oUnseenParams->iLimit = $oParams->iLimit; +// $oUnseenParams->iPrevUidNext = $oParams->iPrevUidNext; +// $oUnseenParams->iThreadUid = $oParams->iThreadUid; + return $this->GetUids($oUnseenParams, 'unseen', $oInfo); } /** @@ -804,9 +648,7 @@ class MailClient */ public function MessageList(MessageListParams $oParams) : MessageCollection { - if (!\MailSo\Base\Validator::RangeInt($oParams->iOffset, 0) || - !\MailSo\Base\Validator::RangeInt($oParams->iLimit, 0, 999)) - { + if (0 > $oParams->iOffset || 0 > $oParams->iLimit || 999 < $oParams->iLimit) { throw new \InvalidArgumentException; } @@ -818,172 +660,120 @@ class MailClient $oMessageCollection->Limit = $oParams->iLimit; $oMessageCollection->Search = $sSearch; $oMessageCollection->ThreadUid = $oParams->iThreadUid; - $oMessageCollection->Filtered = '' !== \MailSo\Config::$MessageListPermanentFilter; +// $oMessageCollection->Filtered = '' !== $this->oImapClient->Settings->search_filter; - list($iMessageRealCount, $iMessageUnseenCount, $iUidNext, $iHighestModSeq) = $this->initFolderValues($oParams->sFolderName); - // Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot - $oInfo = $this->oImapClient->FolderSelect($oParams->sFolderName); - $oInfo->UNSEEN = $iMessageUnseenCount; - $oInfo->HIGHESTMODSEQ = $iHighestModSeq; -// $oInfo->Hash = $this->GenerateFolderHash($oInfo->FolderName, $oInfo->MESSAGES, $oInfo->UIDNEXT, $oInfo->HIGHESTMODSEQ); + $oInfo = $this->oImapClient->FolderStatusAndSelect($oParams->sFolderName); $oMessageCollection->FolderInfo = $oInfo; + $oMessageCollection->totalEmails = $oInfo->MESSAGES; - $aUids = array(); - $aAllThreads = []; - - $bUseSortIfSupported = $oParams->bUseSortIfSupported && $this->oImapClient->IsSupported('SORT'); - - $bUseThreads = $oParams->bUseThreads ? - ($this->oImapClient->IsSupported('THREAD=REFS') || $this->oImapClient->IsSupported('THREAD=REFERENCES') || $this->oImapClient->IsSupported('THREAD=ORDEREDSUBJECT')) : false; - - if ($oParams->iThreadUid && !$bUseThreads) - { + $bUseThreads = $oParams->bUseThreads && $this->oImapClient->CapabilityValue('THREAD'); +// && ($this->oImapClient->hasCapability('THREAD=REFS') || $this->oImapClient->hasCapability('THREAD=REFERENCES') || $this->oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')); + if ($oParams->iThreadUid && !$bUseThreads) { throw new \InvalidArgumentException('THREAD not supported'); } - if (!$oParams->oCacher || !($oParams->oCacher instanceof \MailSo\Cache\CacheClient)) - { - $oParams->oCacher = null; - } - - $oMessageCollection->FolderHash = $this->GenerateFolderHash( - $oParams->sFolderName, $iMessageRealCount, $iUidNext, $oInfo->HIGHESTMODSEQ ?: 0 - ); - - if (!$oParams->iThreadUid) - { + if (!$oParams->iThreadUid) { $oMessageCollection->NewMessages = $this->getFolderNextMessageInformation( - $oParams->sFolderName, $oParams->iPrevUidNext, $iUidNext + $oParams->sFolderName, $oParams->iPrevUidNext, $oInfo->UIDNEXT ); } - $bSearch = false; - $bMessageListOptimization = 0 < \MailSo\Config::$MessageListCountLimitTrigger && - \MailSo\Config::$MessageListCountLimitTrigger < $iMessageRealCount; + if ($oInfo->MESSAGES) { + $bUseSort = $oParams->bUseSort || $oParams->sSort; + $aAllThreads = []; + $aUnseenUIDs = []; + $aUids = []; - if ($bMessageListOptimization) - { - $bUseSortIfSupported = false; - $bUseThreads = false; - } - - if ($iMessageRealCount && !$bMessageListOptimization) - { - if ($bUseThreads) { - $aAllThreads = $this->MessageListThreadsMap($oMessageCollection->FolderName, $oMessageCollection->FolderHash, $oParams->oCacher); -// $iThreadLimit = \MailSo\Config::$LargeThreadLimit; - - if ($oParams->iThreadUid) - { - $aUids = [$oParams->iThreadUid]; - // Only show the selected thread messages - foreach ($aAllThreads as $aMap) { - if (\in_array($oParams->iThreadUid, $aMap)) { - $aUids = $aMap; - break; - } + $message_list_limit = $this->oImapClient->Settings->message_list_limit; + if (0 < $message_list_limit && $message_list_limit < $oInfo->MESSAGES) { +// if ((0 < $message_list_limit && $message_list_limit < $oInfo->MESSAGES) +// || (!$this->oImapClient->hasCapability('SORT') && !$this->oImapClient->CapabilityValue('THREAD'))) { + // Don't use THREAD for speed + $oMessageCollection->Limited = true; + $this->logWrite('List optimization (count: '.$oInfo->MESSAGES.', limit:'.$message_list_limit.')'); + if (\strlen($sSearch)) { + // Don't use SORT for speed + $aUids = $this->GetUids($oParams, $sSearch, $oInfo/*, $bUseSort*/); + } else { + $bUseSort = $this->oImapClient->hasCapability('SORT'); + if (2 > $oInfo->MESSAGES) { + $aRequestIndexes = \array_slice([1], $oParams->iOffset, 1); + } else if ($bUseSort) { + // Attempt to sort REVERSE DATE with a bigger range then $oParams->iLimit + $end = \min($oInfo->MESSAGES, \max(1, $oInfo->MESSAGES - $oParams->iOffset + $oParams->iLimit)); + $start = \max(1, $end - ($oParams->iLimit * 3) + 1); + $oParams->oSequenceSet = new SequenceSet(\range($end, $start), false); + $aRequestIndexes = $this->GetUids($oParams, '', $oInfo, $bUseSort); + // Attempt to get the correct $oParams->iLimit slice + $aRequestIndexes = \array_slice($aRequestIndexes, $oParams->iOffset ? $oParams->iLimit : 0, $oParams->iLimit); + } else { + // Fetch ID's from high to low + $end = \max(1, $oInfo->MESSAGES - $oParams->iOffset); + $start = \max(1, $end - $oParams->iLimit + 1); + $aRequestIndexes = \range($end, $start); } - } - else - { - $aUids = $this->GetUids($oParams, '', - $oMessageCollection->FolderName, $oMessageCollection->FolderHash, $bUseSortIfSupported, $oParams->sSort); - // Remove all threaded UID's except the most recent of each thread - $threadedUids = []; - foreach ($aAllThreads as $aMap) { - unset($aMap[\array_key_last($aMap)]); - $threadedUids = \array_merge($threadedUids, $aMap); - } - $aUids = \array_diff($aUids, $threadedUids); + $this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aRequestIndexes, false)); } } else { - $aUids = $this->GetUids($oParams, '', - $oMessageCollection->FolderName, $oMessageCollection->FolderHash, $bUseSortIfSupported, $oParams->sSort); - } + $aUids = ($bUseThreads && $oParams->iThreadUid) + ? [$oParams->iThreadUid] + : $this->GetUids($oParams, '', $oInfo, $bUseSort); - if ($aUids && \strlen($sSearch)) - { - $aSearchedUids = $this->GetUids($oParams, $sSearch, - $oMessageCollection->FolderName, $oMessageCollection->FolderHash); - if ($bUseThreads && !$oParams->iThreadUid) { - $matchingThreadUids = []; - foreach ($aAllThreads as $aMap) { - if (\array_intersect($aSearchedUids, $aMap)) { - $matchingThreadUids = \array_merge($matchingThreadUids, $aMap); + if ($bUseThreads) { + $aAllThreads = $this->MessageListThreadsMap($oMessageCollection, $oParams->oCacher); + $oMessageCollection->totalThreads = \count($aAllThreads); +// $iThreadLimit = $this->oImapClient->Settings->thread_limit; + if ($oParams->iThreadUid) { + // Only show the selected thread messages + foreach ($aAllThreads as $aMap) { + if (\in_array($oParams->iThreadUid, $aMap)) { + $aUids = $aMap; + break; } + } + $aAllThreads = [$aUids]; + // This only speeds up the search when not cached +// $oParams->oSequenceSet = new SequenceSet($aUids); + } else { + // Remove all threaded UID's except the most recent of each thread + $threadedUids = []; + foreach ($aAllThreads as $aMap) { + unset($aMap[\array_key_last($aMap)]); + $threadedUids = \array_merge($threadedUids, $aMap); + } + $aUids = \array_diff($aUids, $threadedUids); + // Get all unseen + $aUnseenUIDs = $this->MessageListUnseen($oParams, $oInfo); } - $aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids, $matchingThreadUids) { - return \in_array($iUid, $aSearchedUids) || \in_array($iUid, $matchingThreadUids); - }); - } else { - $aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids) { - return \in_array($iUid, $aSearchedUids); - }); - } - } - - $oMessageCollection->MessageResultCount = \count($aUids); - - if (\count($aUids)) - { - $this->MessageListByRequestIndexOrUids( - $oMessageCollection, - new SequenceSet(\array_slice($aUids, $oParams->iOffset, $oParams->iLimit)) - ); - } - } - else if ($iMessageRealCount) - { - if ($this->oLogger) - { - $this->oLogger->Write('List optimization (count: '.$iMessageRealCount. - ', limit:'.\MailSo\Config::$MessageListCountLimitTrigger.')'); - } - - if (\strlen($sSearch)) - { - $aUids = $this->GetUids($oParams, $sSearch, - $oMessageCollection->FolderName, $oMessageCollection->FolderHash); - - $oMessageCollection->MessageResultCount = \count($aUids); - if ($oMessageCollection->MessageResultCount) - { - $this->MessageListByRequestIndexOrUids( - $oMessageCollection, - new SequenceSet(\array_slice($aUids, $oParams->iOffset, $oParams->iLimit)) - ); - } - } - else - { - $oMessageCollection->MessageResultCount = $iMessageRealCount; - - if (1 < $iMessageRealCount) - { - $aRequestIndexes = \array_slice(\array_reverse(\range(1, $iMessageRealCount)), $oParams->iOffset, $oParams->iLimit); - } - else - { - $aRequestIndexes = \array_slice(array(1), $oParams->iOffset, $oParams->iLimit); } - $this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aRequestIndexes, false)); - } - } - - if ($aAllThreads && !$oParams->iThreadUid) - { - foreach ($oMessageCollection as $oMessage) { - $iUid = $oMessage->Uid(); - // Find thread and set it. - // Used by GUI to delete/move the whole thread or other features - foreach ($aAllThreads as $aMap) { - if (\in_array($iUid, $aMap)) { - $oMessage->SetThreads($aMap); - break; + if ($aUids && \strlen($sSearch)) { + $aSearchedUids = $this->GetUids($oParams, $sSearch, $oInfo/*, $bUseSort*/); + if ($bUseThreads && !$oParams->iThreadUid) { + $matchingThreadUids = []; + foreach ($aAllThreads as $aMap) { + if (\array_intersect($aSearchedUids, $aMap)) { + $matchingThreadUids = \array_merge($matchingThreadUids, $aMap); + } + } + $aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids, $matchingThreadUids) { + return \in_array($iUid, $aSearchedUids) || \in_array($iUid, $matchingThreadUids); + }); + } else { + $aUids = \array_filter($aUids, function($iUid) use ($aSearchedUids) { + return \in_array($iUid, $aSearchedUids); + }); } } } + + if (\count($aUids)) { + $oMessageCollection->totalEmails = \count($aUids); + $aUids = \array_slice($aUids, $oParams->iOffset, $oParams->iLimit); + $this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aUids), $aAllThreads, $aUnseenUIDs); + } + } else { + $this->logWrite('No messages in '.$oMessageCollection->FolderName); } return $oMessageCollection; @@ -991,8 +781,7 @@ class MailClient public function FindMessageUidByMessageId(string $sFolderName, string $sMessageId) : ?int { - if (!\strlen($sMessageId)) - { + if (!\strlen($sMessageId)) { throw new \InvalidArgumentException; } @@ -1003,95 +792,52 @@ class MailClient return 1 === \count($aUids) && \is_numeric($aUids[0]) ? (int) $aUids[0] : null; } - public function Folders(string $sParent, string $sListPattern, bool $bUseListSubscribeStatus, int $iOptimizationLimit, bool $bUseListStatus) : ?FolderCollection + public function Folders(string $sParent, string $sListPattern, bool $bUseListSubscribeStatus) : ?FolderCollection { - $aImapSubscribedFoldersHelper = null; - if ($this->oImapClient->IsSupported('LIST-EXTENDED')) { - $bUseListSubscribeStatus = false; - } else if ($bUseListSubscribeStatus) { - //\SnappyMail\Log::warning('IMAP', 'RFC5258 not supported, using LSUB'); +// $this->oImapClient->Settings->disable_list_status + $oFolderCollection = $this->oImapClient->FolderStatusList($sParent, $sListPattern); + if (!$oFolderCollection->count()) { + return null; + } + + if ($bUseListSubscribeStatus && !$this->oImapClient->hasCapability('LIST-EXTENDED')) { +// $this->logWrite('RFC5258 not supported, using LSUB'); +// \SnappyMail\Log::warning('IMAP', 'RFC5258 not supported, using LSUB'); try { - $aSubscribedFolders = $this->oImapClient->FolderSubscribeList($sParent, $sListPattern); - $aImapSubscribedFoldersHelper = array(); - foreach ($aSubscribedFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) - { - $aImapSubscribedFoldersHelper[] = $oImapFolder->FullName(); + $oSubscribedFolders = $this->oImapClient->FolderSubscribeList($sParent, $sListPattern); + foreach ($oSubscribedFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { + isset($oFolderCollection[$oImapFolder->FullName]) + && $oFolderCollection[$oImapFolder->FullName]->setSubscribed(); } } catch (\Throwable $oException) { \SnappyMail\Log::error('IMAP', 'FolderSubscribeList: ' . $oException->getMessage()); - } - } - - $aFolders = $bUseListStatus - ? $this->oImapClient->FolderStatusList($sParent, $sListPattern) - : $this->oImapClient->FolderList($sParent, $sListPattern); - if (!$aFolders) { - return null; - } - - $oFolderCollection = new FolderCollection; - $oFolderCollection->Optimized = 10 < $iOptimizationLimit && \count($aFolders) > $iOptimizationLimit; - - $sINBOX = 'INBOX'; - foreach ($aFolders as $sFullName => /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { - $oMailFolder = new Folder($oImapFolder, - ($bUseListSubscribeStatus && (null === $aImapSubscribedFoldersHelper || \in_array($sFullName, $aImapSubscribedFoldersHelper))) - || $oImapFolder->IsInbox() - ); - if ($oImapFolder->IsInbox()) { - $sINBOX = $sFullName; - } - $aFolders[$sFullName] = $oMailFolder; - - // Add NonExistent folders - $sDelimiter = $oMailFolder->Delimiter(); - $aFolderExplode = \explode($sDelimiter, $sFullName); - \array_pop($aFolderExplode); - while ($aFolderExplode) { - $sNonExistentFolderFullName = \implode($sDelimiter, $aFolderExplode); - if (!isset($aFolders[$sNonExistentFolderFullName])) { - try - { - $aFolders[$sNonExistentFolderFullName] = - Folder::NewNonExistentInstance($sNonExistentFolderFullName, $sDelimiter); - } - catch (\Throwable $oExc) - { - unset($oExc); - } + foreach ($oFolderCollection as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder) { + $oImapFolder->setSubscribed(); } - \array_pop($aFolderExplode); } } - $oFolderCollection->exchangeArray(\array_values($aFolders)); - - $oFolderCollection->TotalCount = \count($aFolders); - return $oFolderCollection; } /** * @throws \InvalidArgumentException */ - public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = '') : ?Folder + public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = '') : ?\MailSo\Imap\Folder { $sFolderNameInUtf8 = \trim($sFolderNameInUtf8); $sFolderParentFullName = \trim($sFolderParentFullName); - if (!\strlen($sFolderNameInUtf8)) - { + if (!\strlen($sFolderNameInUtf8)) { throw new \InvalidArgumentException; } - if (!\strlen($sDelimiter) || \strlen($sFolderParentFullName)) - { + if (!\strlen($sDelimiter) || \strlen($sFolderParentFullName)) { $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sFolderParentFullName); - if (null === $sDelimiter) - { + if (null === $sDelimiter) { // TODO: Translate throw new \MailSo\RuntimeException( \strlen($sFolderParentFullName) @@ -1099,33 +845,29 @@ class MailClient : 'Cannot get folder delimiter.'); } - if (\strlen($sDelimiter) && \strlen($sFolderParentFullName)) - { + if (\strlen($sDelimiter) && \strlen($sFolderParentFullName)) { $sFolderParentFullName .= $sDelimiter; } } - if (\strlen($sDelimiter) && false !== \strpos($sFolderNameInUtf8, $sDelimiter)) - { +/* // Allow non existent parent folders + if (\strlen($sDelimiter) && false !== \strpos($sFolderNameInUtf8, $sDelimiter)) { // TODO: Translate - throw new \MailSo\RuntimeException( - 'New folder name contains delimiter.'); + throw new \MailSo\RuntimeException('New folder name contains delimiter.'); } - +*/ $sFullNameToCreate = $sFolderParentFullName.$sFolderNameInUtf8; - $this->oImapClient->FolderCreate($sFullNameToCreate); + $this->oImapClient->FolderCreate($sFullNameToCreate, $bSubscribeOnCreation); - if ($bSubscribeOnCreation) - { - $this->oImapClient->FolderSubscribe($sFullNameToCreate); + $aFolders = $this->oImapClient->FolderStatusList($sFullNameToCreate, ''); + if (isset($aFolders[$sFullNameToCreate])) { + $oImapFolder = $aFolders[$sFullNameToCreate]; + $bSubscribeOnCreation && $oImapFolder->setSubscribed(); + return $oImapFolder; } - $aFolders = $this->oImapClient->IsSupported('LIST-STATUS') - ? $this->oImapClient->FolderStatusList($sFullNameToCreate, '') - : $this->oImapClient->FolderList($sFullNameToCreate, ''); - $oImapFolder = $aFolders[$sFullNameToCreate]; - return $oImapFolder ? new Folder($oImapFolder, $bSubscribeOnCreation) : null; + return null; } /** @@ -1171,32 +913,26 @@ class MailClient */ protected function folderModify(string $sPrevFolderFullName, string $sNewFolderFullName, bool $bSubscribe) : self { - if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) - { + if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) { throw new \InvalidArgumentException; } - $aSubscribeFolders = array(); - if ($bSubscribe) - { - $aSubscribeFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*'); - foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) - { - $this->oImapClient->FolderUnsubscribe($oFolder->FullName()); + $oSubscribedFolders = array(); + if ($bSubscribe) { + $oSubscribedFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*'); + foreach ($oSubscribedFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) { + $this->oImapClient->FolderUnsubscribe($oFolder->FullName); } } $this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName); - foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) - { - $sFolderFullNameForResubscrine = $oFolder->FullName(); - if (0 === \strpos($sFolderFullNameForResubscrine, $sPrevFolderFullName)) - { - $sNewFolderFullNameForResubscrine = $sNewFolderFullName. - \substr($sFolderFullNameForResubscrine, \strlen($sPrevFolderFullName)); - - $this->oImapClient->FolderSubscribe($sNewFolderFullNameForResubscrine); + foreach ($oSubscribedFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) { + $sFolderFullNameForResubscribe = $oFolder->FullName; + if (\str_starts_with($sFolderFullNameForResubscribe, $sPrevFolderFullName)) { + $this->oImapClient->FolderSubscribe( + $sNewFolderFullName . \substr($sFolderFullNameForResubscribe, \strlen($sPrevFolderFullName)) + ); } } @@ -1205,84 +941,15 @@ class MailClient /** * @throws \InvalidArgumentException - * @throws \MailSo\RuntimeException */ - public function FolderDelete(string $sFolderFullName) : self - { - if (!\strlen($sFolderFullName) || 'INBOX' === $sFolderFullName) { - throw new \InvalidArgumentException; - } - - if ($this->oImapClient->IsSupported('IMAP4rev2')) { - $oInfo = $this->oImapClient->FolderExamine($sFolderFullName); - } else { - $oInfo = $this->oImapClient->FolderStatus($sFolderFullName); - } - if ($oInfo->MESSAGES) { - throw new Exceptions\NonEmptyFolder; - } - - $this->oImapClient->FolderUnsubscribe($sFolderFullName); - - $this->oImapClient->FolderUnselect(); - $this->oImapClient->FolderDelete($sFolderFullName); - - return $this; - } - - /** - * @throws \InvalidArgumentException - */ - public function FolderClear(string $sFolderFullName) : self - { - if (0 < $this->oImapClient->FolderSelect($sFolderFullName)->MESSAGES) { - $this->oImapClient->MessageStoreFlag(new SequenceSet('1:*', false), - array(MessageFlag::DELETED), - StoreAction::ADD_FLAGS_SILENT - ); - $this->oImapClient->FolderExpunge(); - } - return $this; - } - - /** - * @throws \InvalidArgumentException - */ - public function FolderSubscribe(string $sFolderFullName, bool $bSubscribe) : self - { - if (!\strlen($sFolderFullName)) { - throw new \InvalidArgumentException; - } - $this->oImapClient->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName); - return $this; - } - - /** - * @throws \InvalidArgumentException - */ - public function SetLogger(\MailSo\Log\Logger $oLogger) : void + public function SetLogger(?\MailSo\Log\Logger $oLogger) : void { $this->oLogger = $oLogger; $this->oImapClient->SetLogger($oLogger); } - public function GetPersonalNamespace() : string - { - $oNamespace = $this->oImapClient->GetNamespace(); - return $oNamespace ? $oNamespace->GetPersonalNamespace() : ''; - } - public function __call(string $name, array $arguments) /*: mixed*/ { return $this->oImapClient->{$name}(...$arguments); } - - /** - * RFC 5464 - */ - - public function FolderDeleteMetadata($sFolderName, array $aEntries) : void - { - $this->oImapClient->FolderSetMetadata($sFolderName, \array_fill_keys(\array_keys($aEntries), null)); - } } 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 1c34c79c0..26bece670 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 @@ -11,8 +11,8 @@ namespace MailSo\Mail; -use \MailSo\Base\Utils; -use \MailSo\Imap\Enumerations\FetchType; +use MailSo\Base\Utils; +use MailSo\Imap\Enumerations\FetchType; /** * @category MailSo @@ -20,114 +20,77 @@ use \MailSo\Imap\Enumerations\FetchType; */ class Message implements \JsonSerializable { - private + private string $sFolder = '', - $iUid = 0, $sSubject = '', $sMessageId = '', $sContentType = '', - $iSize = 0, - $iSpamScore = 0, $sSpamResult = '', - $bIsSpam = false, - $bHasVirus = null, - $iInternalTimeStampInUTC = 0, - $iHeaderTimeStampInUTC = 0, - $sHeaderDate = '', -// $aFlags = [], - $aFlagsLowerCase = [], + $sVirusScanned = '', + $InReplyTo = '', + $sPlain = '', + $sHtml = '', + $References = '', + $sDeliveryReceipt = '', + $ReadReceipt = ''; + private ?string /** * https://www.rfc-editor.org/rfc/rfc8474#section-5 */ - $sEmailId = '', - $sThreadId = '', + $sEmailId = null, + $sThreadId = null, + // https://autocrypt.org/level1.html#the-autocrypt-header + $sAutocrypt = ''; + private int + $Uid = 0, + $iSize = 0, + $SpamScore = 0, + $iInternalTimeStampInUTC = 0, + $iHeaderTimeStampInUTC = 0, + $iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL; + + private bool + $bIsSpam = false; + + private ?bool /** - * @var \MailSo\Mime\EmailCollection + * null = not scanned + * true = scanned and infected + * false = scanned and no infection found */ + $bHasVirus = null; + + private array + $SPF = [], + $DKIM = [], + $DMARC = [], +// $aFlags = [], + $aFlagsLowerCase = [], + $UnsubsribeLinks = [], + $aThreadUIDs = [], + $aThreadUnseenUIDs = []; + + private ?array $DraftInfo = null; + private ?array $pgpSigned = null; + private ?array $pgpEncrypted = null; + + private ?\MailSo\Mime\EmailCollection $oFrom = null, $oSender = null, $oReplyTo = null, $oDeliveredTo = null, $oTo = null, $oCc = null, - $oBcc = null, + $oBcc = null; - $sInReplyTo = '', + private ?AttachmentCollection + $Attachments = null; - $sPlain = '', - $sHtml = '', - - /** - * @var AttachmentCollection - */ - $oAttachments = null, - - /** - * @var array - */ - $aDraftInfo = null, - - $sReferences = '', - - /** - * @var int - */ - $iPriority, - - $sDeliveryReceipt = '', - - $sReadReceipt = '', - - // https://autocrypt.org/level1.html#the-autocrypt-header - $sAutocrypt = '', - - $aUnsubsribeLinks = array(), - - $aThreads = array(), - - $aPgpSigned = null, - $aPgpEncrypted = null; - - function __construct() + function __get($k) { - $this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL; - } - - public function Plain() : string - { - return $this->sPlain; - } - - public function Html() : string - { - return $this->sHtml; - } - - public function PgpSigned() : ?array - { - return $this->aPgpSigned; - } - - public function PgpEncrypted() : ?array - { - return $this->aPgpEncrypted; - } - - public function Folder() : string - { - return $this->sFolder; - } - - public function Uid() : int - { - return $this->iUid; - } - - public function MessageId() : string - { - return $this->sMessageId; + return \property_exists($this, $k) ? $this->$k : null; } public function Subject() : string @@ -135,164 +98,48 @@ class Message implements \JsonSerializable return $this->sSubject; } - public function ContentType() : string - { - return $this->sContentType; - } - - public function Size() : int - { - return $this->iSize; - } - - public function SpamScore() : int - { - return $this->iSpamScore; - } - - private function setSpamScore($value) : void - { - $this->iSpamScore = \intval(\max(0, \min(100, $value))); - } - - public function SpamResult() : string - { - return $this->sSpamResult; - } - - public function IsSpam() : bool - { - return $this->bIsSpam; - } - - /** - * null = not scanned - * true = scanned and infected - * false = scanned and no infection found - */ - public function HasVirus() : ?bool - { - return $this->bHasVirus; - } - - public function InternalTimeStampInUTC() : int - { - return $this->iInternalTimeStampInUTC; - } - - public function HeaderTimeStampInUTC() : int - { - return $this->iHeaderTimeStampInUTC; - } - - public function HeaderDate() : string - { - return $this->sHeaderDate; - } - public function From() : ?\MailSo\Mime\EmailCollection { return $this->oFrom; } - public function Priority() : int + public function Uid() : int { - return $this->iPriority; - } - - public function Sender() : ?\MailSo\Mime\EmailCollection - { - return $this->oSender; - } - - public function ReplyTo() : ?\MailSo\Mime\EmailCollection - { - return $this->oReplyTo; - } - - public function DeliveredTo() : ?\MailSo\Mime\EmailCollection - { - return $this->oDeliveredTo; - } - - public function To() : ?\MailSo\Mime\EmailCollection - { - return $this->oTo; - } - - public function Cc() : ?\MailSo\Mime\EmailCollection - { - return $this->oCc; - } - - public function Bcc() : ?\MailSo\Mime\EmailCollection - { - return $this->oBcc; + return $this->Uid; } public function Attachments() : ?AttachmentCollection { - return $this->oAttachments; + return $this->Attachments; } - public function InReplyTo() : string + private function setSpamScore($value) : void { - return $this->sInReplyTo; + $this->SpamScore = \intval(\max(0, \min(100, $value))); } - public function References() : string + public function SetThreads(array $aThreadUIDs) { - return $this->sReferences; + $this->aThreadUIDs = $aThreadUIDs; } - public function DeliveryReceipt() : string + public function SetThreadUnseen(array $aUnseenUIDs) { - return $this->sDeliveryReceipt; + $this->aThreadUnseenUIDs = $aUnseenUIDs; } - public function ReadReceipt() : string - { - return $this->sReadReceipt; - } - - public function UnsubsribeLinks() : array - { - return $this->aUnsubsribeLinks; - } - - public function ReadingConfirmation() : string - { - return $this->ReadReceipt(); - } - - public function DraftInfo() : ?array - { - return $this->aDraftInfo; - } - - public function Threads() : array - { - return $this->aThreads; - } - - public function SetThreads(array $aThreads) - { - $this->aThreads = $aThreads; - } - - public static function NewFetchResponseInstance(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self + public static function fromFetchResponse(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self { $oMessage = new self; - if (!$oBodyStructure) - { + if (!$oBodyStructure) { $oBodyStructure = $oFetchResponse->GetFetchBodyStructure(); } $aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS) ?: []; $oMessage->sFolder = $sFolder; - $oMessage->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); + $oMessage->Uid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); $oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE); // $oMessage->aFlags = $aFlags; $oMessage->aFlagsLowerCase = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $aFlags)); @@ -300,31 +147,28 @@ class Message implements \JsonSerializable $oFetchResponse->GetFetchValue(FetchType::INTERNALDATE) ); - $oMessage->sEmailId = $oFetchResponse->GetFetchValue(FetchType::EMAILID) -// ?: $oFetchResponse->GetFetchValue('X-GUID') - ?: $oFetchResponse->GetFetchValue('X-GM-MSGID'); - $oMessage->sThreadId = $oFetchResponse->GetFetchValue(FetchType::THREADID) - ?: $oFetchResponse->GetFetchValue('X-GM-THRID'); + // https://www.rfc-editor.org/rfc/rfc8474 + $aEmailId = $oFetchResponse->GetFetchValue(FetchType::EMAILID); + $oMessage->sEmailId = $aEmailId ? $aEmailId[0] : $oFetchResponse->GetFetchValue('X-GM-MSGID'); +// $oMessage->sEmailId = $oMessage->sEmailId ?: $oFetchResponse->GetFetchValue('X-GUID'); + $aThreadId = $oFetchResponse->GetFetchValue(FetchType::THREADID); + $oMessage->sThreadId = $aThreadId ? $aThreadId[0] : $oFetchResponse->GetFetchValue('X-GM-THRID'); $sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : ''; $sHeaders = $oFetchResponse->GetHeaderFieldsValue(); - if (\strlen($sHeaders)) - { - $oHeaders = new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset); - + $oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null; + if ($oHeaders) { $sContentTypeCharset = $oHeaders->ParameterValue( \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::CHARSET ); - if (\strlen($sContentTypeCharset)) - { + if (\strlen($sContentTypeCharset)) { $sCharset = Utils::NormalizeCharset($sContentTypeCharset); } - if (\strlen($sCharset)) - { + if (\strlen($sCharset)) { $oHeaders->SetParentCharset($sCharset); } @@ -339,48 +183,32 @@ class Message implements \JsonSerializable $oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect); $oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect); - if ($oMessage->oFrom) { - $oHeaders->PopulateEmailColectionByDkim($oMessage->oFrom); - } - $oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect); $oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect); $oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect); - $oMessage->sInReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO); - $oMessage->sReferences = Utils::StripSpaces( + $oMessage->InReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO); + $oMessage->References = Utils::StripSpaces( $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES)); - $sHeaderDate = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE); - $oMessage->sHeaderDate = $sHeaderDate; - $oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate); + $oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString( + $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE) + ); // Priority - $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL; - $sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY); - if (!\strlen($sPriority)) - { - $sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE); - } - if (!\strlen($sPriority)) - { - $sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY); - } - if (\strlen($sPriority)) - { - switch (\str_replace(' ', '', \strtolower($sPriority))) + $sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY) + ?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE) + ?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY); + if (\strlen($sPriority)) { + switch (\substr(\trim($sPriority), 0, 1)) { - case 'high': - case '1(highest)': - case '2(high)': + case 'h': case '1': case '2': $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH; break; - case 'low': - case '4(low)': - case '5(lowest)': + case 'l': case '4': case '5': $oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW; @@ -392,26 +220,19 @@ class Message implements \JsonSerializable $oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::RETURN_RECEIPT_TO)); // Read Receipt - $oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO)); - if (empty($oMessage->sReadReceipt)) - { - $oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO)); + $oMessage->ReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO)); + if (empty($oMessage->ReadReceipt)) { + $oMessage->ReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO)); } // Unsubscribe links - $oMessage->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE); - if (empty($oMessage->aUnsubsribeLinks)) - { - $oMessage->aUnsubsribeLinks = array(); - } - else - { - $oMessage->aUnsubsribeLinks = explode(',', $oMessage->aUnsubsribeLinks); - $oMessage->aUnsubsribeLinks = array_map( + $UnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE); + if ($UnsubsribeLinks) { + $oMessage->UnsubsribeLinks = \array_map( function ($link) { return trim($link, ' <>'); }, - $oMessage->aUnsubsribeLinks + \explode(',', $UnsubsribeLinks) ); } @@ -480,7 +301,22 @@ class Message implements \JsonSerializable } if (\strlen($sType) && \strlen($sFolder) && $iUid) { - $oMessage->aDraftInfo = array($sType, $iUid, $sFolder); + $oMessage->DraftInfo = array($sType, $iUid, $sFolder); + } + } + + $aAuth = $oHeaders->AuthStatuses(); + $oMessage->SPF = $aAuth['spf']; + $oMessage->DKIM = $aAuth['dkim']; + $oMessage->DMARC = $aAuth['dmarc']; + if ($aAuth['dkim'] && $oMessage->oFrom) { + foreach ($oMessage->oFrom as $oEmail) { + $sEmail = $oEmail->GetEmail(); + foreach ($aAuth['dkim'] as $aDkimData) { + if (\strpos($sEmail, $aDkimData[1])) { + $oEmail->SetDkimStatus($aDkimData[0]); + } + } } } @@ -500,16 +336,15 @@ class Message implements \JsonSerializable $oMessage->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset); $oMessage->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset); $oMessage->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset); - $oMessage->sInReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, ''); + $oMessage->InReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, ''); } - if ($oBodyStructure) - { + if ($oBodyStructure) { $gEncryptedParts = $oBodyStructure->SearchByContentType('multipart/encrypted'); foreach ($gEncryptedParts as $oPart) { if ($oPart->IsPgpEncrypted()) { - $oMessage->aPgpEncrypted = [ - 'PartId' => $oPart->SubParts()[1]->PartID() + $oMessage->pgpEncrypted = [ + 'partId' => $oPart->SubParts()[1]->PartID() ]; } } @@ -520,51 +355,47 @@ class Message implements \JsonSerializable continue; } $oPgpSignaturePart = $oPart->SubParts()[1]; - $oMessage->aPgpSigned = [ + $oMessage->pgpSigned = [ // /?/Raw/&q[]=/0/Download/&q[]=/... // /?/Raw/&q[]=/0/View/&q[]=/... - 'BodyPartId' => $oPart->SubParts()[0]->PartID(), - 'SigPartId' => $oPgpSignaturePart->PartID(), - 'MicAlg' => (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') + 'bodyPartId' => $oPart->SubParts()[0]->PartID(), + 'sigPartId' => $oPgpSignaturePart->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') : '' ]; /* // An empty section specification refers to the entire message, including the header. // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. $sPgpText = \trim( - \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['BodyPartId'].'.MIME]')) + \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['bodyPartId'].'.MIME]')) . "\r\n\r\n" - . \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['BodyPartId'].']')) + . \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['bodyPartId'].']')) ); if ($sPgpText) { - $oMessage->aPgpSigned['Body'] = $sPgpText; + $oMessage->pgpSigned['body'] = $sPgpText; } - $sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['SigPartId'].']'); + $sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['sigPartId'].']'); if ($sPgpSignatureText && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) { - $oMessage->aPgpSigned['Signature'] = $oPart->SubParts()[0]->PartID(); + $oMessage->pgpSigned['signature'] = $oPart->SubParts()[0]->PartID(); } */ break; } $aTextParts = $oBodyStructure->GetHtmlAndPlainParts(); - if ($aTextParts) - { + if ($aTextParts) { $sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::UTF_8; $aHtmlParts = array(); $aPlainParts = array(); - foreach ($aTextParts as $oPart) - { + foreach ($aTextParts as $oPart) { $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']'); - if (null === $sText) - { + if (null === $sText) { // TextPartIsTrimmed ? $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>'); } - if (\is_string($sText) && \strlen($sText)) - { + if (\is_string($sText) && \strlen($sText)) { $sText = Utils::DecodeEncodingValue($sText, $oPart->MailEncodingName()); $sText = Utils::ConvertEncoding($sText, Utils::NormalizeCharset($oPart->Charset() ?: $sCharset, true), @@ -574,34 +405,28 @@ class Message implements \JsonSerializable // https://datatracker.ietf.org/doc/html/rfc4880#section-7 // Cleartext Signature - if (!$oMessage->aPgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----')) - { - $oMessage->aPgpSigned = [ - 'BodyPartId' => $oPart->PartID() + if (!$oMessage->pgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----')) { + $oMessage->pgpSigned = [ + 'bodyPartId' => $oPart->PartID() ]; } - if (\str_contains($sText, '-----BEGIN PGP MESSAGE-----')) - { + if (\str_contains($sText, '-----BEGIN PGP MESSAGE-----')) { $keyIds = []; if (\SnappyMail\PGP\GPG::isSupported()) { $GPG = new \SnappyMail\PGP\GPG(''); $keyIds = $GPG->getEncryptedMessageKeys($sText); } - $oMessage->aPgpEncrypted = [ - 'PartId' => $oPart->PartID(), - 'KeyIds' => $keyIds + $oMessage->pgpEncrypted = [ + 'partId' => $oPart->PartID(), + 'keyIds' => $keyIds ]; } - if ('text/html' === $oPart->ContentType()) - { + if ('text/html' === $oPart->ContentType()) { $aHtmlParts[] = $sText; - } - else - { - if ($oPart->IsFlowedFormat()) - { + } else { + if ($oPart->IsFlowedFormat()) { $sText = Utils::DecodeFlowedFormat($sText); } @@ -617,14 +442,12 @@ class Message implements \JsonSerializable } $gAttachmentsParts = $oBodyStructure->SearchAttachmentsParts(); - if ($gAttachmentsParts->valid()) - { - $oMessage->oAttachments = new AttachmentCollection; - foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem) - { + if ($gAttachmentsParts->valid()) { + $oMessage->Attachments = new AttachmentCollection; + foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem) { // if ('application/pgp-keys' === $oAttachmentItem->ContentType()) import ??? - $oMessage->oAttachments->append( - Attachment::NewBodyStructureInstance($oMessage->sFolder, $oMessage->iUid, $oAttachmentItem) + $oMessage->Attachments->append( + new Attachment($oMessage->sFolder, $oMessage->Uid, $oAttachmentItem) ); } } @@ -633,6 +456,29 @@ class Message implements \JsonSerializable return $oMessage; } + public function ETag(string $sClientHash) : string + { + return \md5('MessageHash/' . \implode('/', [ + $this->sFolder, + $this->Uid, + \implode(',', $this->getFlags()), +// \implode(',', $this->aThreadUIDs), + $sClientHash + ])); + } + + // https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1 + // Thunderbird $label1 is same as $Important? + // Thunderbird $label4 is same as $todo? + protected function getFlags() : array + { + return \array_unique(\str_replace( + ['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/], + ['$mdnsent', '\\answered',/* '$junk', '$notjunk', '$submitpending', '$submitted', '$submitted'*/], + $this->aFlagsLowerCase + )); + } + #[\ReturnTypeWillChange] public function jsonSerialize() { @@ -644,55 +490,105 @@ class Message implements \JsonSerializable $this->aFlagsLowerCase ), true); */ - // https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1 - // Thunderbird $label1 is same as $Important? - // Thunderbird $label4 is same as $todo? - $aFlags = \array_unique(\str_replace( - ['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/], - ['$mdnsent', '\\answered',/* '$junk', '$notjunk', '$submitpending', '$submitted', '$submitted'*/], - $this->aFlagsLowerCase - )); + $aAutocrypt = []; + if ($this->sAutocrypt) { + foreach (\explode(';', $this->sAutocrypt) as $entry) { + $entry = \explode('=', \trim($entry), 2); + $aAutocrypt[$entry[0]] = $entry[1]; + } + } - return array( + $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, - 'Uid' => $this->iUid, + 'folder' => $this->sFolder, + 'uid' => $this->Uid, + 'hash' => \md5($this->sFolder . $this->Uid), 'subject' => \trim(Utils::Utf8Clear($this->sSubject)), - 'encrypted' => 'multipart/encrypted' == $this->sContentType || $this->PgpEncrypted(), - 'MessageId' => $this->sMessageId, - 'SpamScore' => $this->bIsSpam ? 100 : $this->iSpamScore, - 'SpamResult' => $this->sSpamResult, - 'IsSpam' => $this->bIsSpam, - 'HasVirus' => $this->bHasVirus, - 'DateTimeStampInUTC' => $this->iInternalTimeStampInUTC, + 'encrypted' => 'multipart/encrypted' == $this->sContentType || $this->pgpEncrypted, + 'messageId' => $this->sMessageId, + '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, // \MailSo\Mime\EmailCollection - 'From' => $this->oFrom, - 'ReplyTo' => $this->oReplyTo, - 'To' => $this->oTo, - 'Cc' => $this->oCc, - 'Bcc' => $this->oBcc, - 'Sender' => $this->oSender, - 'DeliveredTo' => $this->oDeliveredTo, + 'from' => $this->oFrom, + 'replyTo' => $this->oReplyTo, + 'to' => $this->oTo, + 'cc' => $this->oCc, + 'bcc' => $this->oBcc, + 'sender' => $this->oSender, + 'deliveredTo' => $this->oDeliveredTo, - 'Priority' => $this->iPriority, - 'Threads' => $this->aThreads, - 'UnsubsribeLinks' => $this->aUnsubsribeLinks, - 'ReadReceipt' => '', - 'Autocrypt' => $this->sAutocrypt, + 'priority' => $this->iPriority, + 'readReceipt' => $sReadReceipt, + 'autocrypt' => $aAutocrypt ?: null, - 'Attachments' => $this->oAttachments, + 'attachments' => $this->Attachments, - 'Flags' => $aFlags, + 'spf' => $this->SPF, + 'dkim' => $this->DKIM, + 'dmarc' => $this->DMARC, + + 'flags' => $aFlags, + + 'inReplyTo' => $this->InReplyTo, // https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1 'id' => $this->sEmailId, // 'blobId' => $this->sEmailIdBlob, - 'threadId' => $this->sThreadId, +// 'threadId' => $this->sThreadId, // 'mailboxIds' => ['mailboxid'=>true], // 'keywords' => $keywords, - 'size' => $this->iSize, - 'receivedAt' => \gmdate('Y-m-d\\TH:i:s\\Z', $this->iInternalTimeStampInUTC) + 'size' => $this->iSize ); + + if ($this->DraftInfo) { + $result['draftInfo'] = $this->DraftInfo; + } + if ($this->UnsubsribeLinks) { + $result['unsubsribeLinks'] = $this->UnsubsribeLinks; + } + if ($this->References) { + $result['references'] = $this->References; + } + if ($this->sHtml || $this->sPlain) { + $result['html'] = $this->sHtml; + $result['plain'] = $this->sPlain; + } +// $this->GetCapa(Capa::OPEN_PGP) || $this->GetCapa(Capa::GNUPG) + if ($this->pgpSigned) { + $result['pgpSigned'] = $this->pgpSigned; + } + if ($this->pgpEncrypted) { + $result['pgpEncrypted'] = $this->pgpEncrypted; + } + + if ($this->aThreadUIDs) { + $result['threads'] = $this->aThreadUIDs; + } + if ($this->aThreadUnseenUIDs) { + $result['threadUnseen'] = $this->aThreadUnseenUIDs; + } + + return $result; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php index 3cb2e990e..50587840d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageCollection.php @@ -18,52 +18,30 @@ namespace MailSo\Mail; class MessageCollection extends \MailSo\Base\Collection { /** - * @var string + * Amount of UIDs in this list (could be less then total messages when using threads) */ - public $FolderHash = ''; + public int $totalEmails = 0; - /** - * @var int - */ - public $MessageResultCount = 0; + public ?int $totalThreads = null; - /** - * @var string - */ - public $FolderName = ''; + public string $FolderName = ''; - /** - * @var int - */ - public $Offset = 0; + public int $Offset = 0; - /** - * @var int - */ - public $Limit = 0; + public int $Limit = 0; - /** - * @var string - */ - public $Search = ''; + public string $Search = ''; - /** - * @var int - */ - public $ThreadUid = 0; + public int $ThreadUid = 0; // MailSo\Imap\FolderInformation public $FolderInfo = null; - /** - * @var array - */ - public $NewMessages = array(); + public array $NewMessages = array(); - /** - * @var bool - */ - public $Filtered = false; +// public bool $Filtered = false; + + public bool $Limited = false; public function append($oMessage, bool $bToTop = false) : void { @@ -80,16 +58,16 @@ class MessageCollection extends \MailSo\Base\Collection public function jsonSerialize() { return array_merge(parent::jsonSerialize(), array( - 'MessageResultCount' => $this->MessageResultCount, - 'Folder' => $this->FolderName, - 'FolderHash' => $this->FolderHash, - 'FolderInfo' => $this->FolderInfo, - 'ThreadUid' => $this->ThreadUid, - 'NewMessages' => $this->NewMessages, - 'Filtered' => $this->Filtered, - 'Offset' => $this->Offset, - 'Limit' => $this->Limit, - 'Search' => $this->Search + 'totalEmails' => $this->totalEmails, + 'totalThreads' => $this->totalThreads, + 'threadUid' => $this->ThreadUid, + 'newMessages' => $this->NewMessages, +// 'filtered' => $this->Filtered, + 'offset' => $this->Offset, + 'limit' => $this->Limit, + 'search' => $this->Search, + 'limited' => $this->Limited, + 'folder' => $this->FolderInfo )); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php index c8e929860..14686c83d 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MessageListParams.php @@ -13,21 +13,30 @@ namespace MailSo\Mail; class MessageListParams { - public - $sFolderName, // string - $sSearch = '', // string - $oCacher = null, // ?\MailSo\Cache\CacheClient - $bUseSortIfSupported = false, // bool - $bUseThreads = false, // bool - $bHideDeleted = true, // bool - $sSort = ''; // string + public string + $sFolderName, + $sSearch = '', + $sSort = ''; - protected + public ?\MailSo\Cache\CacheClient + $oCacher = null; + + public bool + $bUseSort = true, + $bUseThreads = false, + $bHideDeleted = true; + + protected int $iOffset = 0, - $iLimit = 10, + $iLimit = 0, $iPrevUidNext = 0, // used to check for new messages $iThreadUid = 0; + /** + * Messages with message sequence numbers corresponding to the specified message sequence number set. + */ + public ?\MailSo\Imap\SequenceSet $oSequenceSet = null; + public function __get($k) { return \property_exists($this, $k) ? $this->$k : null; @@ -38,7 +47,22 @@ class MessageListParams if ('i' === $k[0]) { $this->$k = \max(0, (int) $v); } -// \MailSo\Base\Validator::RangeInt($oParams->iOffset, 0) -// \MailSo\Base\Validator::RangeInt($oParams->iLimit, 0, 999) +// 0 > $oParams->iOffset +// 0 > $oParams->iLimit +// 999 < $oParams->iLimit + } + + public function hash() : string + { + return \md5(\implode('-', [ + $this->sFolderName, + $this->iOffset, + $this->iLimit, + $this->bHideDeleted ? '1' : '0', + $this->sSearch, + $this->bUseSort ? $this->sSort : '', + $this->bUseThreads ? $this->iThreadUid : '', + $this->iPrevUidNext + ])); } } 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 a81493c41..904764c80 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 @@ -22,55 +22,41 @@ class Attachment */ private $rResource; - /** - * @var string - */ - private $sFileName; + private string $sFileName; - /** - * @var int - */ - private $iFileSize; +// private int $iFileSize; - /** - * @var string - */ - private $sCID; + private string $sContentID; - /** - * @var bool - */ - private $bIsInline; + private bool $bIsInline; - /** - * @var bool - */ - private $bIsLinked; + private bool $bIsLinked; - /** - * @var array - */ - private $aCustomContentTypeParams; + private array $aCustomContentTypeParams; - /** - * @var string - */ - private $sContentLocation; + private string $sContentLocation; + + private string $sContentType; /** * @param resource $rResource */ function __construct($rResource, string $sFileName, int $iFileSize, bool $bIsInline, - bool $bIsLinked, string $sCID, array $aCustomContentTypeParams = [], string $sContentLocation = '') + bool $bIsLinked, string $sContentID, array $aCustomContentTypeParams = [], + string $sContentLocation = '', string $sContentType = '') { $this->rResource = $rResource; $this->sFileName = $sFileName; - $this->iFileSize = $iFileSize; +// $this->iFileSize = $iFileSize; $this->bIsInline = $bIsInline; $this->bIsLinked = $bIsLinked; - $this->sCID = $sCID; + $this->sContentID = $sContentID; $this->aCustomContentTypeParams = $aCustomContentTypeParams; $this->sContentLocation = $sContentLocation; + $this->sContentType = $sContentType + ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName) + ?: \SnappyMail\File\MimeType::fromFilename($sFileName) + ?: 'application/octet-stream'; } /** @@ -83,7 +69,7 @@ class Attachment public function ContentType() : string { - return \MailSo\Base\Utils::MimeContentType($this->sFileName); + return $this->sContentType; } public function CustomContentTypeParams() : array @@ -91,69 +77,33 @@ class Attachment return $this->aCustomContentTypeParams; } - public function CID() : string - { - return $this->sCID; - } - - public function ContentLocation() : string - { - return $this->sContentLocation; - } - public function FileName() : string { return $this->sFileName; } - public function FileSize() : int - { - return $this->iFileSize; - } - - public function IsInline() : bool + public function isInline() : bool { return $this->bIsInline; } - public function IsImage() : bool + public function isLinked() : bool { - return 'image' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName()); - } - - public function IsArchive() : bool - { - return 'archive' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName()); - } - - public function IsPdf() : bool - { - return 'pdf' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName()); - } - - public function IsDoc() : bool - { - return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName()); - } - - public function IsLinked() : bool - { - return $this->bIsLinked && \strlen($this->sCID); + return $this->bIsLinked && \strlen($this->sContentID); } public function ToPart() : Part { $oAttachmentPart = new Part; - $sFileName = $this->FileName(); - $sCID = $this->CID(); - $sContentLocation = $this->ContentLocation(); + $sFileName = \trim($this->sFileName); + $sContentID = $this->sContentID; + $sContentLocation = $this->sContentLocation; $oContentTypeParameters = null; $oContentDispositionParameters = null; - if (\strlen(\trim($sFileName))) - { + if (\strlen($sFileName)) { $oContentTypeParameters = (new ParameterCollection)->Add(new Parameter( Enumerations\Parameter::NAME, $sFileName)); @@ -165,27 +115,25 @@ class Attachment $oAttachmentPart->Headers->append( new Header(Enumerations\Header::CONTENT_TYPE, - $this->ContentType().';'. - (($oContentTypeParameters) ? ' '.$oContentTypeParameters->ToString() : '') + $this->ContentType(). + ($oContentTypeParameters ? '; '.$oContentTypeParameters : '') ) ); $oAttachmentPart->Headers->append( new Header(Enumerations\Header::CONTENT_DISPOSITION, - ($this->IsInline() ? 'inline' : 'attachment').';'. - (($oContentDispositionParameters) ? ' '.$oContentDispositionParameters->ToString() : '') + ($this->isInline() ? 'inline' : 'attachment'). + ($oContentDispositionParameters ? '; '.$oContentDispositionParameters : '') ) ); - if (\strlen($sCID)) - { + if (\strlen($sContentID)) { $oAttachmentPart->Headers->append( - new Header(Enumerations\Header::CONTENT_ID, $sCID) + new Header(Enumerations\Header::CONTENT_ID, $sContentID) ); } - if (\strlen($sContentLocation)) - { + if (\strlen($sContentLocation)) { $oAttachmentPart->Headers->append( new Header(Enumerations\Header::CONTENT_LOCATION, $sContentLocation) ); @@ -193,8 +141,7 @@ class Attachment $oAttachmentPart->Body = $this->Resource(); - if ('message/rfc822' !== \strtolower($this->ContentType())) - { + if ('message/rfc822' !== \strtolower($this->ContentType())) { $oAttachmentPart->Headers->append( new Header( Enumerations\Header::CONTENT_TRANSFER_ENCODING, @@ -202,17 +149,13 @@ class Attachment ) ); - if (\is_resource($oAttachmentPart->Body)) - { - if (!\MailSo\Base\StreamWrappers\Binary::IsStreamRemembed($oAttachmentPart->Body)) - { - $oAttachmentPart->Body = - \MailSo\Base\StreamWrappers\Binary::CreateStream($oAttachmentPart->Body, - \MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName( - \MailSo\Base\Enumerations\Encoding::BASE64, false)); + if (\is_resource($oAttachmentPart->Body) && !\MailSo\Base\StreamWrappers\Binary::IsStreamRemembed($oAttachmentPart->Body)) { + $oAttachmentPart->Body = + \MailSo\Base\StreamWrappers\Binary::CreateStream($oAttachmentPart->Body, + \MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName( + \MailSo\Base\Enumerations\Encoding::BASE64, false)); - \MailSo\Base\StreamWrappers\Binary::RememberStream($oAttachmentPart->Body); - } + \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 047712430..fbb5de7ec 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 @@ -17,33 +17,18 @@ namespace MailSo\Mime; */ class Email implements \JsonSerializable { - /** - * @var string - */ - private $sDisplayName; + private string $sDisplayName; - /** - * @var string - */ - private $sEmail; + private string $sEmail; - /** - * @var string - */ - private $sDkimStatus; - - /** - * @var string - */ - private $sDkimValue; + private string $sDkimStatus = Enumerations\DkimStatus::NONE; /** * @throws \InvalidArgumentException */ function __construct(string $sEmail, string $sDisplayName = '') { - if (!\strlen(\trim($sEmail))) - { + if (!\strlen(\trim($sEmail)) && !\strlen(\trim($sDisplayName))) { throw new \InvalidArgumentException; } @@ -51,9 +36,6 @@ class Email implements \JsonSerializable \MailSo\Base\Utils::Trim($sEmail), true); $this->sDisplayName = \MailSo\Base\Utils::Trim($sDisplayName); - - $this->sDkimStatus = Enumerations\DkimStatus::NONE; - $this->sDkimValue = ''; } /** @@ -62,8 +44,7 @@ class Email implements \JsonSerializable public static function Parse(string $sEmailAddress) : self { $sEmailAddress = \MailSo\Base\Utils::Trim($sEmailAddress); - if (!\strlen(\trim($sEmailAddress))) - { + if (!\strlen(\trim($sEmailAddress))) { throw new \InvalidArgumentException; } @@ -79,20 +60,16 @@ class Email implements \JsonSerializable $iEndIndex = 0; $iCurrentIndex = 0; - while ($iCurrentIndex < \strlen($sEmailAddress)) - { + while ($iCurrentIndex < \strlen($sEmailAddress)) { switch ($sEmailAddress[$iCurrentIndex]) { // case '\'': case '"': // $sQuoteChar = $sEmailAddress[$iCurrentIndex]; - if ((!$bInName) && (!$bInAddress) && (!$bInComment)) - { + if (!$bInName && !$bInAddress && !$bInComment) { $bInName = true; $iStartIndex = $iCurrentIndex; - } - else if ((!$bInAddress) && (!$bInComment)) - { + } else if (!$bInAddress && !$bInComment) { $iEndIndex = $iCurrentIndex; $sName = \substr($sEmailAddress, $iStartIndex + 1, $iEndIndex - $iStartIndex - 1); $sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iEndIndex - $iStartIndex + 1); @@ -103,10 +80,8 @@ class Email implements \JsonSerializable } break; case '<': - if ((!$bInName) && (!$bInAddress) && (!$bInComment)) - { - if ($iCurrentIndex > 0 && \strlen($sName) === 0) - { + if (!$bInName && !$bInAddress && !$bInComment) { + if ($iCurrentIndex > 0 && !\strlen($sName)) { $sName = \substr($sEmailAddress, 0, $iCurrentIndex); } @@ -115,8 +90,7 @@ class Email implements \JsonSerializable } break; case '>': - if ($bInAddress) - { + if ($bInAddress) { $iEndIndex = $iCurrentIndex; $sEmail = \substr($sEmailAddress, $iStartIndex + 1, $iEndIndex - $iStartIndex - 1); $sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iEndIndex - $iStartIndex + 1); @@ -127,15 +101,13 @@ class Email implements \JsonSerializable } break; case '(': - if ((!$bInName) && (!$bInAddress) && (!$bInComment)) - { + if (!$bInName && !$bInAddress && !$bInComment) { $bInComment = true; $iStartIndex = $iCurrentIndex; } break; case ')': - if ($bInComment) - { + if ($bInComment) { $iEndIndex = $iCurrentIndex; $sComment = \substr($sEmailAddress, $iStartIndex + 1, $iEndIndex - $iStartIndex - 1); $sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iEndIndex - $iStartIndex + 1); @@ -146,28 +118,23 @@ class Email implements \JsonSerializable } break; case '\\': - $iCurrentIndex++; + ++$iCurrentIndex; break; } - $iCurrentIndex++; + ++$iCurrentIndex; } - if (\strlen($sEmail) === 0) - { + if (!\strlen($sEmail)) { $aRegs = array(''); - if (\preg_match('/[^@\s]+@\S+/i', $sEmailAddress, $aRegs) && isset($aRegs[0])) - { + if (\preg_match('/[^@\s]+@\S+/i', $sEmailAddress, $aRegs) && isset($aRegs[0])) { $sEmail = $aRegs[0]; - } - else - { + } else { $sName = $sEmailAddress; } } - if ((\strlen($sEmail) > 0) && (\strlen($sName) == 0) && (\strlen($sComment) == 0)) - { + if (\strlen($sEmail) && !\strlen($sName) && !\strlen($sComment)) { $sName = \str_replace($sEmail, '', $sEmailAddress); } @@ -196,16 +163,6 @@ class Email implements \JsonSerializable return $this->sDisplayName; } - public function GetDkimStatus() : string - { - return $this->sDkimStatus; - } - - public function GetDkimValue() : string - { - return $this->sDkimValue; - } - public function GetAccountName() : string { return \MailSo\Base\Utils::GetAccountNameFromEmail($this->GetEmail(false)); @@ -216,17 +173,9 @@ class Email implements \JsonSerializable return \MailSo\Base\Utils::GetDomainFromEmail($this->GetEmail($bIdn)); } - public function SetDkimStatusAndValue(string $sDkimStatus, string $sDkimValue = '') + public function SetDkimStatus(string $sDkimStatus) { $this->sDkimStatus = Enumerations\DkimStatus::normalizeValue($sDkimStatus); - $this->sDkimValue = $sDkimValue; - } - - public function ToArray(bool $bIdn = false, bool $bDkim = true) : array - { - return $bDkim ? - array($this->sDisplayName, $this->GetEmail($bIdn), $this->sDkimStatus, $this->sDkimValue) : - array($this->sDisplayName, $this->GetEmail($bIdn)); } public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string @@ -234,19 +183,16 @@ class Email implements \JsonSerializable $sReturn = ''; $sDisplayName = \str_replace('"', '\"', $this->sDisplayName); - if ($bConvertSpecialsName) - { - $sDisplayName = 0 === \strlen($sDisplayName) ? '' : \MailSo\Base\Utils::EncodeUnencodedValue( - \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, - $sDisplayName); + if ($bConvertSpecialsName) { + $sDisplayName = \strlen($sDisplayName) + ? \MailSo\Base\Utils::EncodeUnencodedValue(\MailSo\Base\Enumerations\Encoding::BASE64_SHORT, $sDisplayName) + : ''; } - $sDisplayName = 0 === \strlen($sDisplayName) ? '' : '"'.$sDisplayName.'"'; - if (\strlen($this->sEmail)) - { + $sDisplayName = \strlen($sDisplayName) ? '"'.$sDisplayName.'"' : ''; + if (\strlen($this->sEmail)) { $sReturn = $this->GetEmail($bIdn); - if (\strlen($sDisplayName)) - { + if (\strlen($sDisplayName)) { $sReturn = $sDisplayName.' <'.$sReturn.'>'; } } @@ -254,22 +200,19 @@ class Email implements \JsonSerializable return \trim($sReturn); } + public function __toString() : string + { + return $this->ToString(); + } + #[\ReturnTypeWillChange] public function jsonSerialize() { -/* - $BIMI = ''; - if (Enumerations\DkimStatus::PASS == $this->GetDkimStatus()) { - $BIMI = \SnappyMail\DNS\BIMI($this->GetDomain()); - } -*/ return array( '@Object' => 'Object/Email', - 'Name' => \MailSo\Base\Utils::Utf8Clear($this->GetDisplayName()), - 'Email' => \MailSo\Base\Utils::Utf8Clear($this->GetEmail(true)), - 'DkimStatus' => $this->GetDkimStatus(), - 'DkimValue' => $this->GetDkimValue() -// 'BIMI' => $BIMI + 'name' => \MailSo\Base\Utils::Utf8Clear($this->GetDisplayName()), + 'email' => \MailSo\Base\Utils::Utf8Clear($this->GetEmail(true)), + 'dkimStatus' => $this->sDkimStatus ); } } 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 a0f36e711..ba60a0887 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,21 +33,9 @@ class EmailCollection extends \MailSo\Base\Collection parent::append($oEmail, $bToTop); } - public function ToArray() : array - { - $aReturn = array(); - foreach ($this as $oEmail) - { - $aReturn[] = $oEmail->ToArray(); - } - - return $aReturn; - } - public function MergeWithOtherCollection(EmailCollection $oEmails) : self { - foreach ($oEmails as $oEmail) - { + foreach ($oEmails as $oEmail) { $this->append($oEmail); } @@ -58,11 +46,9 @@ class EmailCollection extends \MailSo\Base\Collection { $aReturn = array(); - foreach ($this as $oEmail) - { + foreach ($this as $oEmail) { $sEmail = $oEmail->GetEmail(); - if (!isset($aReturn[$sEmail])) - { + if (!isset($aReturn[$sEmail])) { $aReturn[$sEmail] = $oEmail; } } @@ -73,22 +59,25 @@ class EmailCollection extends \MailSo\Base\Collection public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string { $aReturn = array(); - foreach ($this as $oEmail) - { + foreach ($this as $oEmail) { $aReturn[] = $oEmail->ToString($bConvertSpecialsName, $bIdn); } return \implode(', ', $aReturn); } + public function __toString() : string + { + return $this->ToString(); + } + private function parseEmailAddresses(string $sRawEmails) : void { // $sRawEmails = \MailSo\Base\Utils::Trim($sRawEmails); $sRawEmails = \trim($sRawEmails); $sWorkingRecipientsLen = \strlen($sRawEmails); - if (!$sWorkingRecipientsLen) - { + if (!$sWorkingRecipientsLen) { return; } @@ -102,59 +91,49 @@ class EmailCollection extends \MailSo\Base\Collection $iCurrentPos = 0; - while ($iCurrentPos < $sWorkingRecipientsLen) - { + while ($iCurrentPos < $sWorkingRecipientsLen) { switch ($sRawEmails[$iCurrentPos]) { case '\'': case '"': - if (!$bIsInQuotes) - { + if (!$bIsInQuotes) { $sChQuote = $sRawEmails[$iCurrentPos]; $bIsInQuotes = true; - } - else if ($sChQuote == $sRawEmails[$iCurrentPos]) - { + } else if ($sChQuote == $sRawEmails[$iCurrentPos]) { $bIsInQuotes = false; } break; case '<': - if (!$bIsInAngleBrackets) - { + if (!$bIsInAngleBrackets) { $bIsInAngleBrackets = true; - if ($bIsInQuotes) - { + if ($bIsInQuotes) { $bIsInQuotes = false; } } break; case '>': - if ($bIsInAngleBrackets) - { + if ($bIsInAngleBrackets) { $bIsInAngleBrackets = false; } break; case '(': - if (!$bIsInBrackets) - { + if (!$bIsInBrackets) { $bIsInBrackets = true; } break; case ')': - if ($bIsInBrackets) - { + if ($bIsInBrackets) { $bIsInBrackets = false; } break; case ',': case ';': - if (!$bIsInAngleBrackets && !$bIsInBrackets && !$bIsInQuotes) - { + if (!$bIsInAngleBrackets && !$bIsInBrackets && !$bIsInQuotes) { $iEmailEndPos = $iCurrentPos; try @@ -175,8 +154,7 @@ class EmailCollection extends \MailSo\Base\Collection ++$iCurrentPos; } - if ($iEmailStartPos < $iCurrentPos) - { + if ($iEmailStartPos < $iCurrentPos) { try { $this->append( @@ -186,4 +164,11 @@ class EmailCollection extends \MailSo\Base\Collection catch (\InvalidArgumentException $oException) {} } } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_slice($this->getArrayCopy(), 0, 100); + } + } 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 5d0e4e738..db239c2a8 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 @@ -44,6 +44,7 @@ abstract class Header CONTENT_DISPOSITION = 'Content-Disposition', CONTENT_DESCRIPTION = 'Content-Description', CONTENT_ID = 'Content-ID', +// CONTENT_BASE = 'Content-Base', // rfc2110 CONTENT_LOCATION = 'Content-Location', RECEIVED_SPF = 'Received-SPF', diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php deleted file mode 100644 index 625b587b6..000000000 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/MimeType.php +++ /dev/null @@ -1,41 +0,0 @@ -sFullValue = \trim($sValue); $this->sEncodedValueForReparse = ''; - if (\strlen($sEncodedValueForReparse) && $this->IsReparsed()) - { + if (\strlen($sEncodedValueForReparse) && $this->IsReparsed()) { $this->sEncodedValueForReparse = \trim($sEncodedValueForReparse); } - if (\strlen($this->sFullValue) && $this->IsParameterized()) - { + if (\strlen($this->sFullValue) && $this->IsParameterized()) { $aRawExplode = \explode(';', $this->sFullValue, 2); - if (2 === \count($aRawExplode)) - { + if (2 === \count($aRawExplode)) { $this->sValue = $aRawExplode[0]; $this->oParameters = new ParameterCollection($aRawExplode[1]); - } - else - { + } else { $this->sValue = $this->sFullValue; } - } - else - { + } else { $this->sValue = $this->sFullValue; } @@ -89,14 +64,12 @@ class Header public static function NewInstanceFromEncodedString(string $sEncodedLines, string $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1) : Header { - if (empty($sIncomingCharset)) - { + if (empty($sIncomingCharset)) { $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1; } $aParts = \explode(':', \str_replace("\r", '', $sEncodedLines), 2); - if (isset($aParts[0]) && isset($aParts[1]) && \strlen($aParts[0]) && \strlen($aParts[1])) - { + if (isset($aParts[0]) && isset($aParts[1]) && \strlen($aParts[0]) && \strlen($aParts[1])) { return new self( \trim($aParts[0]), \trim(\MailSo\Base\Utils::DecodeHeaderValue(\trim($aParts[1]), $sIncomingCharset)), @@ -130,8 +103,7 @@ class Header public function SetParentCharset(string $sParentCharset) : Header { - if ($this->sParentCharset !== $sParentCharset && $this->IsReparsed() && \strlen($this->sEncodedValueForReparse)) - { + if ($this->sParentCharset !== $sParentCharset && $this->IsReparsed() && \strlen($this->sEncodedValueForReparse)) { $this->initInputData( $this->sName, \trim(\MailSo\Base\Utils::DecodeHeaderValue($this->sEncodedValueForReparse, $sParentCharset)), @@ -154,22 +126,12 @@ class Header $this->oParameters->setParameter($sName, $sValue); } - private function wordWrapHelper(string $sValue, string $sGlue = "\r\n ") : string - { - return \trim(\substr(\wordwrap($this->NameWithDelimitrom().$sValue, - 74, $sGlue - ), \strlen($this->NameWithDelimitrom()))); - } - public function __toString() : string { $sResult = $this->sFullValue; - if ($this->IsSubject()) - { - if (!\MailSo\Base\Utils::IsAscii($sResult) && - \function_exists('iconv_mime_encode')) - { + if ($this->IsSubject()) { + if (!\MailSo\Base\Utils::IsAscii($sResult) && \function_exists('iconv_mime_encode')) { $aPreferences = array( // 'scheme' => \MailSo\Base\Enumerations\Encoding::QUOTED_PRINTABLE_SHORT, 'scheme' => \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, @@ -182,20 +144,20 @@ class Header return \iconv_mime_encode($this->Name(), $sResult, $aPreferences); } } - else if ($this->IsParameterized() && 0 < $this->oParameters->Count()) + else if ($this->IsParameterized() && 0 < $this->oParameters->count()) { $sResult = $this->sValue.'; '.$this->oParameters->ToString(true); } else if ($this->IsEmail()) { $oEmailCollection = new EmailCollection($this->sFullValue); - if ($oEmailCollection && 0 < $oEmailCollection->Count()) - { - $sResult = $oEmailCollection->ToString(true, false); + if ($oEmailCollection && $oEmailCollection->count()) { + $sResult = $oEmailCollection->ToString(true); } } - return $this->NameWithDelimitrom().$this->wordWrapHelper($sResult); + // https://www.rfc-editor.org/rfc/rfc2822#section-2.1.1 + return \wordwrap($this->NameWithDelimitrom() . $sResult, 78, "\r\n "); } public function IsSubject() : bool 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 1125fddb1..4697cfd23 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 @@ -18,12 +18,9 @@ namespace MailSo\Mime; class HeaderCollection extends \MailSo\Base\Collection { - protected $sRawHeaders = ''; + protected string $sRawHeaders = ''; - /** - * @var string - */ - protected $sParentCharset = ''; + protected string $sParentCharset = ''; function __construct(string $sRawHeaders = '', bool $bStoreRawHeaders = true, string $sParentCharset = '') { @@ -79,12 +76,9 @@ class HeaderCollection extends \MailSo\Base\Collection public function GetAsEmailCollection(string $sHeaderName, bool $bCharsetAutoDetect = false) : ?EmailCollection { - $oResult = null; - $sValue = $this->ValueByName($sHeaderName, $bCharsetAutoDetect); - if (\strlen($sValue)) { - $oResult = new EmailCollection($sValue); - } - return $oResult && 0 < $oResult->Count() ? $oResult : null; + return new EmailCollection( + $this->ValueByName($sHeaderName, $bCharsetAutoDetect) + ); } public function ParametersByName(string $sHeaderName) : ?ParameterCollection @@ -149,40 +143,29 @@ class HeaderCollection extends \MailSo\Base\Collection } $sFirstChar = \substr($sHeadersValue, 0, 1); - if ($sFirstChar !== ' ' && $sFirstChar !== "\t" && false === \strpos($sHeadersValue, ':')) - { + if ($sFirstChar !== ' ' && $sFirstChar !== "\t" && false === \strpos($sHeadersValue, ':')) { continue; } - else if (null !== $sName && ($sFirstChar === ' ' || $sFirstChar === "\t")) - { + if (null !== $sName && ($sFirstChar === ' ' || $sFirstChar === "\t")) { $sValue = \is_null($sValue) ? '' : $sValue; - if ('?=' === \substr(\rtrim($sHeadersValue), -2)) - { + if ('?=' === \substr(\rtrim($sHeadersValue), -2)) { $sHeadersValue = \rtrim($sHeadersValue); } - if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2)) - { + if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2)) { $sHeadersValue = \ltrim($sHeadersValue); } - if ('=?' === \substr($sHeadersValue, 0, 2)) - { + if ('=?' === \substr($sHeadersValue, 0, 2)) { $sValue .= $sHeadersValue; - } - else - { + } else { $sValue .= "\n".$sHeadersValue; } - } - else - { - if (null !== $sName) - { + } else { + if (null !== $sName) { $oHeader = Header::NewInstanceFromEncodedString($sName.': '.$sValue, $this->sParentCharset); - if ($oHeader) - { + if ($oHeader) { $this->append($oHeader); } @@ -194,18 +177,15 @@ class HeaderCollection extends \MailSo\Base\Collection $sName = $aHeaderParts[0]; $sValue = isset($aHeaderParts[1]) ? $aHeaderParts[1] : ''; - if ('?=' === \substr(\rtrim($sValue), -2)) - { + if ('?=' === \substr(\rtrim($sValue), -2)) { $sValue = \rtrim($sValue); } } } - if (null !== $sName) - { + if (null !== $sName) { $oHeader = Header::NewInstanceFromEncodedString($sName.': '.$sValue, $this->sParentCharset); - if ($oHeader) - { + if ($oHeader) { $this->append($oHeader); } } @@ -213,72 +193,46 @@ class HeaderCollection extends \MailSo\Base\Collection return $this; } - public function DkimStatuses() : array + /** + * https://www.rfc-editor.org/rfc/rfc8601 + * dkim=pass header.d=domain.tld header.s=s1 header.b=F2SfoZWw; + * spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld"; + * dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none) + */ + public function AuthStatuses() : array { - $aResult = array(); - + $aResult = [ + 'dkim' => [], + 'dmarc' => [], + 'spf' => [] + ]; $aHeaders = $this->ValuesByName(Enumerations\Header::AUTHENTICATION_RESULTS); - if (\count($aHeaders)) - { - foreach ($aHeaders as $sHeaderValue) - { - $sStatus = ''; - $sHeader = ''; - $sDkimLine = ''; - - $aMatch = array(); - - $sHeaderValue = \preg_replace('/[\r\n\t\s]+/', ' ', $sHeaderValue); - - if (\preg_match('/dkim=.+/i', $sHeaderValue, $aMatch) && !empty($aMatch[0])) - { - $sDkimLine = $aMatch[0]; - - $aMatch = array(); - if (\preg_match('/dkim=([a-zA-Z0-9]+)/i', $sDkimLine, $aMatch) && !empty($aMatch[1])) - { - $sStatus = $aMatch[1]; - } - - $aMatch = array(); - if (\preg_match('/header\.(d|i|from)=([^\s;]+)/i', $sDkimLine, $aMatch) && !empty($aMatch[2])) - { - $sHeader = \trim($aMatch[2]); - } - - if (!empty($sStatus) && !empty($sHeader)) - { - $aResult[] = array($sStatus, $sHeader, $sDkimLine); - } + if (\count($aHeaders)) { + $aHeaders = \implode(';', $aHeaders); + $aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders); + $aHeaders = \explode(';', $aHeaders); + foreach ($aHeaders as $sLine) { + $aStatus = array(); + $aHeader = array(); + if (\preg_match("/(dkim|dmarc|spf)=([a-z0-9]+).*?(;|$)/Di", $sLine, $aStatus) + && \preg_match('/(?:header\\.(?:d|i|from)|smtp.mailfrom)="?([^\\s;"]+)/i', $sLine, $aHeader) + ) { + $sType = \strtolower($aStatus[1]); + $aResult[$sType][] = array(\strtolower($aStatus[2]), $aHeader[1], \trim($sLine)); } } } - else - { + if (!\count($aResult['dkim'])) { // X-DKIM-Authentication-Results: signer="hostinger.com" status="pass" $aHeaders = $this->ValuesByName(Enumerations\Header::X_DKIM_AUTHENTICATION_RESULTS); - foreach ($aHeaders as $sHeaderValue) - { - $sStatus = ''; - $sHeader = ''; - - $aMatch = array(); - - $sHeaderValue = \preg_replace('/[\r\n\t\s]+/', ' ', $sHeaderValue); - - if (\preg_match('/status[\s]?=[\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aMatch) && !empty($aMatch[1])) - { - $sStatus = $aMatch[1]; - } - - if (\preg_match('/signer[\s]?=[\s]?"([^";]+)"/i', $sHeaderValue, $aMatch) && !empty($aMatch[1])) - { - $sHeader = \trim($aMatch[1]); - } - - if (!empty($sStatus) && !empty($sHeader)) - { - $aResult[] = array($sStatus, $sHeader, $sHeaderValue); + foreach ($aHeaders as $sHeaderValue) { + $aStatus = array(); + $aHeader = array(); + $sHeaderValue = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $sHeaderValue); + if (\preg_match('/status[\\s]?=[\\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aStatus) && !empty($aStatus[1]) + && \preg_match('/signer[\\s]?=[\\s]?"([^";]+)"/i', $sHeaderValue, $aHeader) && !empty($aHeader[1]) + ) { + $aResult['dkim'][] = array($aStatus[1], \trim($aHeader[1]), $sHeaderValue); } } } @@ -286,21 +240,6 @@ class HeaderCollection extends \MailSo\Base\Collection return $aResult; } - public function PopulateEmailColectionByDkim(EmailCollection $oEmails) : void - { - $aDkimStatuses = $this->DkimStatuses(); - foreach ($oEmails as $oEmail) { - $sEmail = $oEmail->GetEmail(); - foreach ($aDkimStatuses as $aDkimData) { - if (isset($aDkimData[0], $aDkimData[1]) && - $aDkimData[1] === \strstr($sEmail, $aDkimData[1])) - { - $oEmail->SetDkimStatusAndValue($aDkimData[0], empty($aDkimData[2]) ? '' : $aDkimData[2]); - } - } - } - } - public function __toString() : string { return \implode("\r\n", $this->getArrayCopy()); 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 0639cd8e7..e13ddb553 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 @@ -17,10 +17,7 @@ namespace MailSo\Mime; */ class Message extends Part { - /** - * @var array - */ - private $aHeadersValue = array( + private array $aHeadersValue = array( /* Enumerations\Header::BCC => '', Enumerations\Header::CC => '', @@ -42,22 +39,11 @@ class Message extends Part */ ); - /** - * @var AttachmentCollection - */ - private $oAttachmentCollection; + private AttachmentCollection $oAttachmentCollection; - /** - * @var bool - */ - private $bAddEmptyTextPart = true; + private bool $bAddEmptyTextPart = true; - /** - * @var bool - */ - private $bAddDefaultXMailer = true; - - public $messageIdRequired = true; + private bool $bAddDefaultXMailer = true; function __construct() { @@ -177,8 +163,7 @@ class Message extends Part public function SetCustomHeader(string $sHeaderName, string $sValue) : self { $sHeaderName = \trim($sHeaderName); - if (\strlen($sHeaderName)) - { + if (\strlen($sHeaderName)) { $this->aHeadersValue[$sHeaderName] = $sValue; } @@ -194,16 +179,19 @@ class Message extends Part public function SetInReplyTo(string $sInReplyTo) : self { - $this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo; - + $sInReplyTo = \trim($sInReplyTo); + if (\strlen($sInReplyTo)) { + $this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo; + } return $this; } public function SetReferences(string $sReferences) : self { - $this->aHeadersValue[Enumerations\Header::REFERENCES] = - \MailSo\Base\Utils::StripSpaces($sReferences); - + $sReferences = \MailSo\Base\Utils::StripSpaces($sReferences); + if (\strlen($sReferences)) { + $this->aHeadersValue[Enumerations\Header::REFERENCES] = $sReferences; + } return $this; } @@ -215,11 +203,6 @@ class Message extends Part return $this; } - public function SetReadConfirmation(string $sEmail) : self - { - return $this->SetReadReceipt($sEmail); - } - public function SetPriority(int $iValue) : self { $sResult = ''; @@ -236,8 +219,7 @@ class Message extends Part break; } - if (\strlen($sResult)) - { + if (\strlen($sResult)) { $this->aHeadersValue[Enumerations\Header::X_PRIORITY] = $sResult; } @@ -260,8 +242,9 @@ class Message extends Part public function SetTo(EmailCollection $oEmails) : self { - $this->aHeadersValue[Enumerations\Header::TO_] = $oEmails; - + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::TO_] = $oEmails; + } return $this; } @@ -274,22 +257,25 @@ class Message extends Part public function SetReplyTo(EmailCollection $oEmails) : self { - $this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails; - + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails; + } return $this; } public function SetCc(EmailCollection $oEmails) : self { - $this->aHeadersValue[Enumerations\Header::CC] = $oEmails; - + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::CC] = $oEmails; + } return $this; } public function SetBcc(EmailCollection $oEmails) : self { - $this->aHeadersValue[Enumerations\Header::BCC] = $oEmails; - + if ($oEmails->count()) { + $this->aHeadersValue[Enumerations\Header::BCC] = $oEmails; + } return $this; } @@ -313,18 +299,15 @@ class Message extends Part private function generateNewMessageId(string $sHostName = '') : string { - if (0 === \strlen($sHostName)) - { + if (!\strlen($sHostName)) { $sHostName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''; } - if (empty($sHostName) && \MailSo\Base\Utils::FunctionCallable('php_uname')) - { + if (empty($sHostName) && \MailSo\Base\Utils::FunctionCallable('php_uname')) { $sHostName = \php_uname('n'); } - if (empty($sHostName)) - { + if (empty($sHostName)) { $sHostName = 'localhost'; } @@ -361,7 +344,7 @@ class Message extends Part } $oPart->Headers->append( new Header(Enumerations\Header::CONTENT_TYPE, - $aAttachments[0]->ContentType().'; '.$oParameters->ToString()) + $aAttachments[0]->ContentType().'; '.$oParameters) ); if ($resource = $aAttachments[0]->Resource()) { @@ -384,13 +367,10 @@ class Message extends Part if (1 == \count($this->SubParts)) { $oRootPart = $this->SubParts[0]; foreach ($this->oAttachmentCollection as $oAttachment) { - if ($oAttachment->IsLinked()) { + if ($oAttachment->isLinked()) { $oRelatedPart = new Part; $oRelatedPart->Headers->append( - new Header( - Enumerations\Header::CONTENT_TYPE, - Enumerations\MimeType::MULTIPART_RELATED - ) + new Header(Enumerations\Header::CONTENT_TYPE, 'multipart/related') ); $oRelatedPart->SubParts->append($oRootPart); $oRootPart = $oRelatedPart; @@ -399,24 +379,18 @@ class Message extends Part } } else { $oRootPart = new Part; - $oRootPart->Headers->AddByName( - Enumerations\Header::CONTENT_TYPE, - Enumerations\MimeType::MULTIPART_MIXED - ); + $oRootPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/mixed'); $oRootPart->SubParts = $this->SubParts; } $oMixedPart = null; foreach ($this->oAttachmentCollection as $oAttachment) { - if ($oRelatedPart && $oAttachment->IsLinked()) { + if ($oRelatedPart && $oAttachment->isLinked()) { $oRelatedPart->SubParts->append($oAttachment->ToPart()); } else { if (!$oMixedPart) { $oMixedPart = new Part; - $oMixedPart->Headers->AddByName( - Enumerations\Header::CONTENT_TYPE, - Enumerations\MimeType::MULTIPART_MIXED - ); + $oMixedPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/mixed'); $oMixedPart->SubParts->append($oRootPart); $oRootPart = $oMixedPart; } @@ -441,7 +415,7 @@ class Message extends Part $oRootPart->Headers->SetByName(Enumerations\Header::DATE, \gmdate('r'), true); } - if ($this->messageIdRequired && !isset($this->aHeadersValue[Enumerations\Header::MESSAGE_ID])) { + if (!isset($this->aHeadersValue[Enumerations\Header::MESSAGE_ID])) { $oRootPart->Headers->SetByName(Enumerations\Header::MESSAGE_ID, $this->generateNewMessageId(), true); } @@ -455,11 +429,6 @@ class Message extends Part foreach ($this->aHeadersValue as $sName => $mValue) { if (!($bWithoutBcc && \strtolower(Enumerations\Header::BCC) === \strtolower($sName))) { - if (\is_object($mValue)) { - if ($mValue instanceof EmailCollection || $mValue instanceof Email || $mValue instanceof ParameterCollection) { - $mValue = $mValue->ToString(); - } - } $oRootPart->Headers->SetByName($sName, (string) $mValue); } } 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 cc5c2cd4b..2e20ffe09 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 @@ -15,17 +15,11 @@ namespace MailSo\Mime; * @category MailSo * @package Mime */ -class Parameter +class Parameter implements \JsonSerializable { - /** - * @var string - */ - private $sName; + private string $sName; - /** - * @var string - */ - private $sValue; + private string $sValue; function __construct(string $sName, string $sValue) { @@ -66,12 +60,11 @@ class Parameter { $this->Reset(); - $aParts = explode($sSeparator, $sRawParam, 2); + $aParts = \explode($sSeparator, $sRawParam, 2); - $this->sName = trim(trim($aParts[0]), '"\''); - if (2 === count($aParts)) - { - $this->sValue = trim(trim($aParts[1]), '"\''); + $this->sName = \trim(\trim($aParts[0]), '"\''); + if (2 === \count($aParts)) { + $this->sValue = \trim(\trim($aParts[1]), '"\''); } return $this; @@ -79,25 +72,34 @@ class Parameter public function ToString(bool $bConvertSpecialsName = false) : string { - $sResult = ''; - if (\strlen($this->sName)) - { - $sResult = $this->sName.'='; - if ($bConvertSpecialsName && in_array(strtolower($this->sName), array( - strtolower(Enumerations\Parameter::NAME), - strtolower(Enumerations\Parameter::FILENAME) - ))) - { - $sResult .= '"'.\MailSo\Base\Utils::EncodeUnencodedValue( - \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, - $this->sValue).'"'; - } - else - { - $sResult .= '"'.$this->sValue.'"'; - } + if (!\strlen($this->sName)) { + return ''; } - return $sResult; + if ($bConvertSpecialsName && \in_array(\strtolower($this->sName), array( + \strtolower(Enumerations\Parameter::NAME), + \strtolower(Enumerations\Parameter::FILENAME) + ))) + { + return $this->sName . '="' . \MailSo\Base\Utils::EncodeUnencodedValue( + \MailSo\Base\Enumerations\Encoding::BASE64_SHORT, + $this->sValue) . '"'; + } + + return $this->sName . '="' . $this->sValue . '"'; + } + + public function __toString() : string + { + return $this->ToString(); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( + 'name' => $this->sName, + 'value' => $this->sValue + ); } } 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 a556d30b0..af34acb40 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 @@ -21,10 +21,7 @@ class ParameterCollection extends \MailSo\Base\Collection { parent::__construct(); - if (\strlen($sRawParams)) - { - $this->Parse($sRawParams); - } + \strlen($sRawParams) && $this->Parse($sRawParams); } public function append($oParameter, bool $bToTop = false) : void @@ -66,8 +63,7 @@ class ParameterCollection extends \MailSo\Base\Collection $aDataToParse = \explode(';', $sRawParams); - foreach ($aDataToParse as $sParam) - { + foreach ($aDataToParse as $sParam) { $this->append(Parameter::CreateFromParameterLine($sParam)); } @@ -79,18 +75,35 @@ class ParameterCollection extends \MailSo\Base\Collection public function ToString(bool $bConvertSpecialsName = false) : string { $aResult = array(); - foreach ($this as $oParam) - { + foreach ($this as $oParam) { $sLine = $oParam->ToString($bConvertSpecialsName); - if (\strlen($sLine)) - { + if (\strlen($sLine)) { $aResult[] = $sLine; } } - return \count($aResult) ? \implode('; ', $aResult) : ''; } + public function __toString() : string + { + return $this->ToString(); + } + +/* + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = array(); + foreach ($this as $oParam) { + $aResult[$oParam->Name()] = $oParam->Value(); + } + return array( + '@Object' => 'Collection/ParameterCollection', + '@Collection' => $aResult + ); + } +*/ + private function reParseParameters() : void { $aDataToReParse = $this->getArrayCopy(); @@ -99,26 +112,22 @@ class ParameterCollection extends \MailSo\Base\Collection $this->Clear(); $aPreParams = array(); - foreach ($aDataToReParse as $oParam) - { + foreach ($aDataToReParse as $oParam) { $aMatch = array(); $sParamName = $oParam->Name(); if (\preg_match('/([^\*]+)\*([\d]{1,2})\*/', $sParamName, $aMatch) && isset($aMatch[1], $aMatch[2]) && \strlen($aMatch[1]) && \is_numeric($aMatch[2])) { - if (!isset($aPreParams[$aMatch[1]])) - { + if (!isset($aPreParams[$aMatch[1]])) { $aPreParams[$aMatch[1]] = array(); } $sValue = $oParam->Value(); - if (false !== \strpos($sValue, "''")) - { + if (false !== \strpos($sValue, "''")) { $aValueParts = \explode("''", $sValue, 2); - if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) - { + if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) { $sCharset = $aValueParts[0]; $sValue = $aValueParts[1]; } @@ -128,17 +137,14 @@ class ParameterCollection extends \MailSo\Base\Collection } else if (\preg_match('/([^\*]+)\*/', $sParamName, $aMatch) && isset($aMatch[1])) { - if (!isset($aPreParams[$aMatch[1]])) - { + if (!isset($aPreParams[$aMatch[1]])) { $aPreParams[$aMatch[1]] = array(); } $sValue = $oParam->Value(); - if (false !== \strpos($sValue, "''")) - { + if (false !== \strpos($sValue, "''")) { $aValueParts = \explode("''", $sValue, 2); - if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) - { + if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) { $sCharset = $aValueParts[0]; $sValue = $aValueParts[1]; } @@ -152,14 +158,12 @@ class ParameterCollection extends \MailSo\Base\Collection } } - foreach ($aPreParams as $sName => $aValues) - { - ksort($aValues); + foreach ($aPreParams as $sName => $aValues) { + \ksort($aValues); $sResult = \implode(\array_values($aValues)); $sResult = \urldecode($sResult); - if (\strlen($sCharset)) - { + if (\strlen($sCharset)) { $sResult = \MailSo\Base\Utils::ConvertEncoding($sResult, $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php index 492801e44..805484bb8 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Parser.php @@ -23,7 +23,7 @@ abstract class Parser POS_SUBPARTS = 3, POS_CLOSE_BOUNDARY = 4; - private static $LineParts = []; + private static array $LineParts = []; protected static function writeBody(Part $oPart, string $sBuffer) : void { @@ -59,21 +59,18 @@ abstract class Parser $oMimePart = null; $sFirstNotNullCharset = null; - foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) - { + foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) { $sCharset = $oMimePart->HeaderCharset(); - if (\strlen($sCharset)) - { + if (\strlen($sCharset)) { $sFirstNotNullCharset = $sCharset; break; } } $sFirstNotNullCharset = (null !== $sFirstNotNullCharset) - ? $sFirstNotNullCharset : Part::$DefaultCharset; + ? $sFirstNotNullCharset : \MailSo\Base\Enumerations\Charset::ISO_8859_1; - foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) - { + foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) { $sHeaderCharset = $oMimePart->HeaderCharset(); $oMimePart->Headers->SetParentCharset($sHeaderCharset); } @@ -93,53 +90,38 @@ abstract class Parser $sCurrentBoundary = ''; $bIsBoundaryCheck = false; $aHeadersLines = array(); - while (true) - { - if (!$bNotFirstRead) - { + while (true) { + if (!$bNotFirstRead) { $sPrevBuffer = $sBuffer; $sBuffer = ''; } - if (!$bIsOef && !\feof($rStreamHandle)) - { - if (!$bNotFirstRead) - { + if (!$bIsOef && !\feof($rStreamHandle)) { + if (!$bNotFirstRead) { $sBuffer = \fread($rStreamHandle, 8192); - if (false === $sBuffer) - { + if (false === $sBuffer) { break; } - } - else - { + } else { $bNotFirstRead = false; } - } - else if ($bIsOef && !\strlen($sBuffer)) - { + } else if ($bIsOef && !\strlen($sBuffer)) { break; - } - else - { + } else { $bIsOef = true; } - while (true) - { + while (true) { $sCurrentLine = $sPrevBuffer.$sBuffer; - if (self::POS_HEADERS === $iParsePosition) - { + if (self::POS_HEADERS === $iParsePosition) { $iEndLen = 4; $iPos = \strpos($sCurrentLine, "\r\n\r\n", $iOffset); - if (false === $iPos) - { + if (false === $iPos) { $iEndLen = 2; $iPos = \strpos($sCurrentLine, "\n\n", $iOffset); } - if (false !== $iPos) - { + if (false !== $iPos) { $aHeadersLines[] = \substr($sCurrentLine, $iOffset, $iPos + $iEndLen - $iOffset); $oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset()); @@ -156,42 +138,31 @@ abstract class Parser $iOffset = $iPos + $iEndLen; $iParsePosition = self::POS_BODY; continue; - } - else - { + } else { $iBufferLen = \strlen($sPrevBuffer); - if ($iBufferLen > $iOffset) - { + if ($iBufferLen > $iOffset) { $aHeadersLines[] = \substr($sPrevBuffer, $iOffset); $iOffset = 0; - } - else - { + } else { $iOffset -= $iBufferLen; } break; } - } - else if (self::POS_BODY === $iParsePosition) - { + } else if (self::POS_BODY === $iParsePosition) { $iPos = false; $sBoundaryLen = 0; $bIsBoundaryEnd = false; $bCurrentPartBody = false; $bIsBoundaryCheck = \count($aBoundaryStack); - foreach ($aBoundaryStack as $sKey => $sBoundary) - { - if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset))) - { - if ($sCurrentBoundary === $sBoundary) - { + foreach ($aBoundaryStack as $sKey => $sBoundary) { + if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset))) { + if ($sCurrentBoundary === $sBoundary) { $bCurrentPartBody = true; } $sBoundaryLen = \strlen($sBoundary); - if ('--' === \substr($sCurrentLine, $iPos + $sBoundaryLen, 2)) - { + if ('--' === \substr($sCurrentLine, $iPos + $sBoundaryLen, 2)) { $sBoundaryLen += 2; $bIsBoundaryEnd = true; unset($aBoundaryStack[$sKey]); @@ -203,54 +174,42 @@ abstract class Parser } } - if (false !== $iPos) - { + if (false !== $iPos) { static::writeBody($oPart, \substr($sCurrentLine, $iOffset, $iPos - $iOffset)); $iOffset = $iPos; - if ($bCurrentPartBody) - { + if ($bCurrentPartBody) { $iParsePosition = self::POS_SUBPARTS; continue; } return; - } - else - { + } else { $iBufferLen = \strlen($sPrevBuffer); - if ($iBufferLen > $iOffset) - { + if ($iBufferLen > $iOffset) { static::writeBody($oPart, \substr($sPrevBuffer, $iOffset)); $iOffset = 0; - } - else - { + } else { $iOffset -= $iBufferLen; } break; } - } - else if (self::POS_SUBPARTS === $iParsePosition) - { + } else if (self::POS_SUBPARTS === $iParsePosition) { $iPos = false; $iBoundaryLen = 0; $bIsBoundaryEnd = false; $bCurrentPartBody = false; $bIsBoundaryCheck = \count($aBoundaryStack); - foreach ($aBoundaryStack as $sKey => $sBoundary) - { - if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset))) - { - if ($sCurrentBoundary === $sBoundary) - { + foreach ($aBoundaryStack as $sKey => $sBoundary) { + $iPos = \strpos($sCurrentLine, $sBoundary, $iOffset); + if (false !== $iPos) { + if ($sCurrentBoundary === $sBoundary) { $bCurrentPartBody = true; } $iBoundaryLen = \strlen($sBoundary); - if ('--' === \substr($sCurrentLine, $iPos + $iBoundaryLen, 2)) - { + if ('--' === \substr($sCurrentLine, $iPos + $iBoundaryLen, 2)) { $iBoundaryLen += 2; $bIsBoundaryEnd = true; unset($aBoundaryStack[$sKey]); @@ -261,8 +220,7 @@ abstract class Parser } } - if (false !== $iPos && $bCurrentPartBody) - { + if (false !== $iPos && $bCurrentPartBody) { $iOffset = $iPos + $iBoundaryLen; $oSubPart = new Part; @@ -274,39 +232,27 @@ abstract class Parser static::$LineParts[] = $oSubPart; //$iParsePosition = self::POS_HEADERS; unset($oSubPart); - } - else - { + } else { return; } } } } - if (\strlen($sPrevBuffer)) - { - if (self::POS_HEADERS === $iParsePosition) - { + if (\strlen($sPrevBuffer)) { + if (self::POS_HEADERS === $iParsePosition) { $aHeadersLines[] = ($iOffset < \strlen($sPrevBuffer)) ? \substr($sPrevBuffer, $iOffset) : $sPrevBuffer; $oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset()); $aHeadersLines = array(); + } else if (!$bIsBoundaryCheck && self::POS_BODY === $iParsePosition) { + static::writeBody($oPart, ($iOffset < \strlen($sPrevBuffer)) + ? \substr($sPrevBuffer, $iOffset) : $sPrevBuffer); } - else if (self::POS_BODY === $iParsePosition) - { - if (!$bIsBoundaryCheck) - { - static::writeBody($oPart, ($iOffset < \strlen($sPrevBuffer)) - ? \substr($sPrevBuffer, $iOffset) : $sPrevBuffer); - } - } - } - else - { - if (self::POS_HEADERS === $iParsePosition && \count($aHeadersLines)) - { + } else { + if (self::POS_HEADERS === $iParsePosition && \count($aHeadersLines)) { $oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset()); $aHeadersLines = array(); } 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 014c9008b..b40864e4d 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 @@ -17,15 +17,7 @@ namespace MailSo\Mime; */ class Part { - /** - * @var string - */ - public static $DefaultCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1; - - /** - * @var HeaderCollection - */ - public $Headers; + public HeaderCollection $Headers; /** * @var resource @@ -37,10 +29,7 @@ class Part */ public $Raw = null; - /** - * @var PartCollection - */ - public $SubParts; + public PartCollection $SubParts; function __construct() { @@ -63,29 +52,14 @@ class Part return \trim(\strtolower($this->Headers->ValueByName(Enumerations\Header::CONTENT_TYPE))); } - public function ContentID() : string - { - return \trim($this->Headers->ValueByName(Enumerations\Header::CONTENT_ID)); - } - - public function ContentLocation() : string - { - return \trim($this->Headers->ValueByName(Enumerations\Header::CONTENT_LOCATION)); - } - public function IsFlowedFormat() : bool { - $bResult = false; - if ($this->Headers) - { - $bResult = 'flowed' === \trim(\strtolower($this->Headers->ParameterValue( - Enumerations\Header::CONTENT_TYPE, - Enumerations\Parameter::FORMAT))); + $bResult = 'flowed' === \trim(\strtolower($this->Headers->ParameterValue( + Enumerations\Header::CONTENT_TYPE, + Enumerations\Parameter::FORMAT))); - if ($bResult && \in_array($this->MailEncodingName(), array('base64', 'quoted-printable'))) - { - $bResult = false; - } + if ($bResult && \in_array($this->MailEncodingName(), array('base64', 'quoted-printable'))) { + $bResult = false; } return $bResult; @@ -93,19 +67,14 @@ class Part public function FileName() : string { - $sResult = ''; - if ($this->Headers) - { - $sResult = \trim($this->Headers->ParameterValue( - Enumerations\Header::CONTENT_DISPOSITION, - Enumerations\Parameter::FILENAME)); + $sResult = \trim($this->Headers->ParameterValue( + Enumerations\Header::CONTENT_DISPOSITION, + Enumerations\Parameter::FILENAME)); - if (!\strlen($sResult)) - { - $sResult = \trim($this->Headers->ParameterValue( - Enumerations\Header::CONTENT_TYPE, - Enumerations\Parameter::NAME)); - } + if (!\strlen($sResult)) { + $sResult = \trim($this->Headers->ParameterValue( + Enumerations\Header::CONTENT_TYPE, + Enumerations\Parameter::NAME)); } return $sResult; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php index e5f7f4395..bd7b8fa7c 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/PartCollection.php @@ -17,7 +17,7 @@ namespace MailSo\Mime; */ class PartCollection extends \MailSo\Base\Collection { - protected $sBoundary; + protected string $sBoundary = ''; public function append($oPart, bool $bToTop = false) : void { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php index f1c0ad52e..624e028c2 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/ConnectSettings.php @@ -15,66 +15,84 @@ namespace MailSo\Net; * @category MailSo * @package Net */ -class ConnectSettings +class ConnectSettings implements \JsonSerializable { - public - $host, + public string $host; - $port, + public int $port; - // none, TLS, STARTTLS - $type = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT, + /** + * stream timeout in seconds + */ + public int $timeout = 10; - // https://www.php.net/context.ssl - $ssl = [ -// 'peer_name' => '', -// 'peer_fingerprint' => '', // string | array - 'verify_peer' => true, - 'verify_peer_name' => true, - 'allow_self_signed' => false, -// 'cafile' => '', -// 'capath' => '', + // none, TLS, STARTTLS + public int $type = Enumerations\ConnectionSecurityType::AUTO_DETECT; +// public int $type = Enumerations\ConnectionSecurityType::NONE; -// 'ciphers' => 'HIGH:!SSLv2:!SSLv3', - 'SNI_enabled' => true, - 'disable_compression' => true, - 'security_level' => 1, + public SSLContext $ssl; +// public bool $tls_weak = false; -// 'local_cert' => '', -// 'local_pk' => '', -// 'passphrase' => '', + // Authentication settings use by all child classes + public bool $useAuth = true; + public bool $shortLogin = false; + public array $SASLMechanisms = [ + // https://github.com/the-djmaze/snappymail/issues/182 + 'SCRAM-SHA3-512', + 'SCRAM-SHA-512', + 'SCRAM-SHA-256', + 'SCRAM-SHA-1', +// 'CRAM-MD5', + 'PLAIN', + 'LOGIN' + ]; + public string $Login = ''; + public string $Password = ''; + public string $ProxyAuthUser = ''; + public string $ProxyAuthPassword = ''; -// 'verify_depth' => 0, -// 'capture_peer_cert' => false, -// 'capture_peer_cert_chain' => false, - ]; - - function __construct() + public function __construct() { - // TODO: This should be moved to \RainLoop\Model\Domain - $oConfig = \RainLoop\API::Config(); - $this->ssl['verify_peer'] = !!$oConfig->Get('ssl', 'verify_certificate', false); - $this->ssl['verify_peer_name'] = !!$oConfig->Get('ssl', 'verify_certificate', false); - $this->ssl['allow_self_signed'] = !!$oConfig->Get('ssl', 'allow_self_signed', true); - $this->ssl['security_level'] = (int) $oConfig->Get('ssl', 'security_level', 1); -// $this->ssl['local_cert'] = (string) $oConfig->Get('ssl', 'client_cert', ''); -// $this->ssl['cafile'] = (string) $oConfig->Get('ssl', 'cafile', ''); -// $this->ssl['capath'] = (string) $oConfig->Get('ssl', 'capath', ''); + $this->ssl = new SSLContext; + } + + public static function Host() : string + { + return \SnappyMail\IDN::toAscii($this->host); } public static function fromArray(array $aSettings) : self { - $object = new self; - $object->host = $aSettings['Host']; - $object->port = $aSettings['Port']; - $object->type = $aSettings['Secure']; - $object->ssl['verify_peer'] = !!$aSettings['VerifySsl']; - $object->ssl['verify_peer_name'] = !!$aSettings['VerifySsl']; - $object->ssl['allow_self_signed'] = !!$aSettings['AllowSelfSigned']; - if (!empty($aSettings['ClientCert'])) { - $object->ssl['local_cert'] = $aSettings['ClientCert']; + $object = new static; + $object->host = $aSettings['host']; + $object->port = $aSettings['port']; + $object->type = isset($aSettings['type']) ? $aSettings['type'] : $aSettings['secure']; + if (isset($aSettings['timeout'])) { + $object->timeout = $aSettings['timeout']; } + $object->shortLogin = !empty($aSettings['shortLogin']); + $object->ssl = SSLContext::fromArray($aSettings['ssl'] ?? []); + if (isset($aSettings['sasl'])) { + $object->SASLMechanisms = $aSettings['sasl']; + } +// $object->tls_weak = !empty($aSettings['tls_weak']); return $object; } + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return array( +// '@Object' => 'Object/ConnectSettings', + 'host' => $this->host, + 'port' => $this->port, + 'type' => $this->type, + 'timeout' => $this->timeout, + 'shortLogin' => $this->shortLogin, + 'sasl' => $this->SASLMechanisms, + 'ssl' => $this->ssl +// 'tls_weak' => $this->tls_weak + ); + } + } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketCanNotConnectToHostException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketCanNotConnectToHostException.php index a2bee442f..8c62cfd84 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketCanNotConnectToHostException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/Exceptions/SocketCanNotConnectToHostException.php @@ -18,15 +18,9 @@ namespace MailSo\Net\Exceptions; */ class SocketCanNotConnectToHostException extends ConnectionException { - /** - * @var string - */ - private $sSocketMessage; + private string $sSocketMessage; - /** - * @var int - */ - private $iSocketCode; + private int $iSocketCode; public function __construct(string $sSocketMessage = '', int $iSocketCode = 0, string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php index ed0e78ba4..70a112b46 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/NetClient.php @@ -17,65 +17,26 @@ namespace MailSo\Net; */ abstract class NetClient { + use \MailSo\Log\Inherit; + /** * @var resource */ private $rConnect = null; - /** - * @var bool - */ - private $bUnreadBuffer = false; + private bool $bUnreadBuffer = false; - /** - * @var bool - */ - protected $bRunningCallback = false; + protected bool $bRunningCallback = false; - /** - * @var string - */ - protected $sResponseBuffer = ''; + private string $sConnectedHost = ''; - /** - * @var int - */ - protected $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE; + private bool $ssl = false; - /** - * @var string - */ - private $sConnectedHost = ''; + private int $iConnectTimeOut = 10; - /** - * @var int - */ - private $iConnectedPort = 0; + private float $iStartConnectTime = 0; - /** - * @var bool - */ - private $bSecure = false; - - /** - * @var int - */ - private $iConnectTimeOut = 10; - - /** - * @var int - */ - private $iSocketTimeOut = 10; - - /** - * @var int - */ - private $iStartConnectTime = 0; - - /** - * @var \MailSo\Log\Logger - */ - protected $oLogger = null; + public ConnectSettings $Settings; public function __destruct() { @@ -91,15 +52,9 @@ abstract class NetClient return $this->sConnectedHost; } - public function GetConnectedPort() : int + public function SetTimeOuts(int $iConnectTimeOut = 10) : void { - return $this->iConnectedPort; - } - - public function SetTimeOuts(int $iConnectTimeOut = 10, int $iSocketTimeOut = 10) : void - { - $this->iConnectTimeOut = max(5, $iConnectTimeOut); - $this->iSocketTimeOut = max(5, $iSocketTimeOut); + $this->iConnectTimeOut = \max(5, $iConnectTimeOut); } /** @@ -116,65 +71,56 @@ abstract class NetClient } /** + * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException - * @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Net\Exceptions\SocketAlreadyConnectedException * @throws \MailSo\Net\Exceptions\SocketCanNotConnectToHostException */ public function Connect(ConnectSettings $oSettings) : void { $oSettings->host = \trim($oSettings->host); - if (!\strlen($oSettings->host) || !\MailSo\Base\Validator::PortInt($oSettings->port)) { - $this->writeLogException( - new \MailSo\Base\Exceptions\InvalidArgumentException, - \LOG_ERR, true); + if (!\strlen($oSettings->host) || 0 > $oSettings->port || 65535 < $oSettings->port) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } - if ($this->IsConnected()) - { - $this->writeLogException( - new Exceptions\SocketAlreadyConnectedException, - \LOG_ERR, true); + if ($this->IsConnected()) { + $this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR, false); +// $this->Disconnect(); + return; } + $this->Settings = $oSettings; + $sErrorStr = ''; $iErrorNo = 0; $this->sConnectedHost = $oSettings->host; - $this->iConnectedPort = $oSettings->port; - $this->iSecurityType = $oSettings->type; - $this->bSecure = \MailSo\Net\Enumerations\ConnectionSecurityType::UseSSL( - $this->iConnectedPort, $this->iSecurityType); - if (!\preg_match('/^[a-z0-9._]{2,8}:\/\//i', $this->sConnectedHost)) - { - $this->sConnectedHost = ($this->bSecure ? 'ssl://' : 'tcp://').$this->sConnectedHost; -// $this->sConnectedHost = ($this->bSecure ? 'ssl://' : '').$this->sConnectedHost; + $this->ssl = \MailSo\Net\Enumerations\ConnectionSecurityType::UseSSL($this->Settings->port, $this->Settings->type); + + if (!\preg_match('/^[a-z0-9._]{2,8}:\/\//i', $this->sConnectedHost)) { + $this->sConnectedHost = ($this->ssl ? 'ssl://' : 'tcp://') . $this->sConnectedHost; +// $this->sConnectedHost = ($this->ssl ? 'ssl://' : '') . $this->sConnectedHost; } - if (!$this->bSecure && \MailSo\Net\Enumerations\ConnectionSecurityType::SSL === $this->iSecurityType) - { + if (!$this->ssl && \MailSo\Net\Enumerations\ConnectionSecurityType::SSL === $this->Settings->type) { $this->writeLogException( new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('SSL isn\'t supported: ('.\implode(', ', \stream_get_transports()).')'), - \LOG_ERR, true); + \LOG_ERR); } $this->iStartConnectTime = \microtime(true); - $this->writeLog('Start connection to "'.$this->sConnectedHost.':'.$this->iConnectedPort.'"'); + $this->writeLog('Start connection to "'.$this->sConnectedHost.':'.$this->Settings->port.'"'); - $aStreamContextSettings = array( - 'ssl' => $oSettings->ssl - ); - - \MailSo\Hooks::Run('Net.NetClient.StreamContextSettings/Filter', array(&$aStreamContextSettings)); - - $rStreamContext = \stream_context_create($aStreamContextSettings); + $rStreamContext = \stream_context_create(array( + 'ssl' => $oSettings->ssl->jsonSerialize() + )); \set_error_handler(array($this, 'capturePhpErrorWithException')); try { - $this->rConnect = \stream_socket_client($this->sConnectedHost.':'.$this->iConnectedPort, + $this->rConnect = \stream_socket_client($this->sConnectedHost.':'.$this->Settings->port, $iErrorNo, $sErrorStr, $this->iConnectTimeOut, STREAM_CLIENT_CONNECT, $rStreamContext); } catch (\Throwable $oExc) @@ -187,23 +133,19 @@ abstract class NetClient $this->writeLog('Connect ('.($this->rConnect ? 'success' : 'failed').')'); - if (!$this->rConnect) - { + if (!$this->rConnect) { $this->writeLogException( new Exceptions\SocketCanNotConnectToHostException( \MailSo\Base\Locale::ConvertSystemString($sErrorStr), (int) $iErrorNo, - 'Can\'t connect to host "'.$this->sConnectedHost.':'.$this->iConnectedPort.'"' - ), \LOG_NOTICE, true); + 'Can\'t connect to host "'.$this->sConnectedHost.':'.$this->Settings->port.'"' + ) + ); } $this->writeLog((\microtime(true) - $this->iStartConnectTime).' (raw connection)', \LOG_DEBUG); - if ($this->rConnect) - { - if (\MailSo\Base\Utils::FunctionCallable('stream_set_timeout')) - { - \stream_set_timeout($this->rConnect, $this->iSocketTimeOut); - } + if ($this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_set_timeout')) { + \stream_set_timeout($this->rConnect, \max(5, $oSettings->timeout)); } } @@ -212,27 +154,21 @@ abstract class NetClient return $this->rConnect && !empty(\stream_get_meta_data($this->rConnect)['crypto']); } - public function EnableCrypto(bool $insecure = false) + public function EnableCrypto() : void { - $bError = true; + $bSuccess = false; if ($this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_socket_enable_crypto')) { $crypto_method = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && OPENSSL_VERSION_NUMBER >= 0x10101000) { $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT; } - if ($insecure) { - $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; - } - if (\stream_socket_enable_crypto($this->rConnect, true, $crypto_method)) { - $bError = false; - } +// if ($this->Settings->tls_weak) { +// $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; +// } + $bSuccess = \stream_socket_enable_crypto($this->rConnect, true, $crypto_method); } - if ($bError) { - $this->writeLogException( - new \MailSo\RuntimeException('Cannot enable STARTTLS.'), - \LOG_ERR, true); - } + $bSuccess || $this->writeLogException(new \MailSo\RuntimeException('Cannot enable STARTTLS.'), \LOG_ERR); } /** @@ -241,29 +177,26 @@ abstract class NetClient */ public function Disconnect() : void { - if ($this->rConnect) - { - if (!$this->bUnreadBuffer && !$this->bRunningCallback) - { + if ($this->rConnect) { + if (!$this->bUnreadBuffer && !$this->bRunningCallback) { $this->Logout(); } $bResult = \fclose($this->rConnect); - $this->writeLog('Disconnected from "'.$this->sConnectedHost.':'.$this->iConnectedPort.'" ('. + $this->writeLog('Disconnected from "'.$this->sConnectedHost.':'.$this->Settings->port.'" ('. (($bResult) ? 'success' : 'unsuccess').')'); - if ($this->iStartConnectTime) - { + if ($this->iStartConnectTime) { $this->writeLog((\microtime(true) - $this->iStartConnectTime).' (net session)', \LOG_DEBUG); - $this->iStartConnectTime = 0; } - $this->rConnect = null; } } + abstract public function supportsAuthType(string $sasl_type) : bool; +// abstract public function Login(ConnectSettings $oSettings) : self; abstract public function Logout() : void; public function IsConnected(bool $bThrowExceptionOnFalse = false) : bool @@ -272,21 +205,11 @@ abstract class NetClient return true; } if ($bThrowExceptionOnFalse) { - $this->writeLogException( - new Exceptions\SocketConnectionDoesNotAvailableException, - \LOG_ERR, true); + $this->writeLogException(new Exceptions\SocketConnectionDoesNotAvailableException, \LOG_ERR); } return false; } - /** - * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException - */ - public function IsConnectedWithException() : void - { - $this->IsConnected(true); - } - public function StreamContextParams() : array { return $this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_context_get_options') @@ -300,7 +223,7 @@ abstract class NetClient protected function sendRaw(string $sRaw, bool $bWriteToLog = true, string $sFakeRaw = '') : void { if ($this->bUnreadBuffer) { - $this->writeLogException(new Exceptions\SocketUnreadBufferException, \LOG_ERR, true); + $this->writeLogException(new Exceptions\SocketUnreadBufferException, \LOG_ERR); } $sRaw .= "\r\n"; @@ -311,13 +234,10 @@ abstract class NetClient } $mResult = \fwrite($this->rConnect, $sRaw); - if (false === $mResult) - { + if (false === $mResult) { $this->IsConnected(true); - $this->writeLogException(new Exceptions\SocketWriteException, \LOG_ERR, true); - } - else if ($bWriteToLog) - { + $this->writeLogException(new Exceptions\SocketWriteException, \LOG_ERR); + } else if ($bWriteToLog) { $this->writeLogWithCrlf('> '.($bFake ? $sFakeRaw : $sRaw)); } } @@ -326,112 +246,69 @@ abstract class NetClient * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException * @throws \MailSo\Net\Exceptions\SocketReadException */ - protected function getNextBuffer(?int $iReadLen = null, bool $bForceLogin = false) : void + protected function getNextBuffer(?int $iReadLen = null) : ?string { - if (null === $iReadLen) - { - $this->sResponseBuffer = \fgets($this->rConnect); - } - else - { - $this->sResponseBuffer = ''; + if (null === $iReadLen) { + $sResponseBuffer = \fgets($this->rConnect); + } else { + $sResponseBuffer = ''; $iRead = $iReadLen; - while (0 < $iRead) - { + while (0 < $iRead) { $sAddRead = \fread($this->rConnect, $iRead); - if (false === $sAddRead) - { - $this->sResponseBuffer = false; + if (false === $sAddRead) { + $sResponseBuffer = false; break; } - - $this->sResponseBuffer .= $sAddRead; + $sResponseBuffer .= $sAddRead; $iRead -= \strlen($sAddRead); } } - if (false === $this->sResponseBuffer) - { + if (false === $sResponseBuffer) { $this->IsConnected(true); $this->bUnreadBuffer = true; - $aSocketStatus = \stream_get_meta_data($this->rConnect); - if (isset($aSocketStatus['timed_out']) && $aSocketStatus['timed_out']) - { - $this->writeLogException( - new Exceptions\SocketReadTimeoutException, - \LOG_ERR, true); - } - else - { + if (isset($aSocketStatus['timed_out']) && $aSocketStatus['timed_out']) { + $this->writeLogException(new Exceptions\SocketReadTimeoutException, \LOG_ERR); + } else { $this->writeLog('Stream Meta: '.\print_r($aSocketStatus, true), \LOG_ERR); + $this->writeLogException(new Exceptions\SocketReadException, \LOG_ERR); + } + return null; + } - $this->writeLogException( - new Exceptions\SocketReadException, - \LOG_ERR, true); - } - } - else - { - $iReadedLen = \strlen($this->sResponseBuffer); - if (null === $iReadLen || $bForceLogin) - { - $iLimit = 5000; // 5KB - if ($iLimit < $iReadedLen) - { - $this->writeLogWithCrlf('[cutted:'.$iReadedLen.'] < '.\substr($this->sResponseBuffer, 0, $iLimit).'...'); - } - else - { -// $this->writeLogWithCrlf('< '.$this->sResponseBuffer, //.' ['.$iReadedLen.']', - $this->writeLogWithCrlf('< '.$this->sResponseBuffer); - } - } - else - { - $this->writeLog('Received '.$iReadedLen.'/'.$iReadLen.' bytes.'); - } + $iReadBytes = \strlen($sResponseBuffer); +// $iReadLen && $this->writeLog('Received '.$iReadBytes.'/'.$iReadLen.' bytes.'); + $iLimit = 5000; // 5KB + if ($iLimit < $iReadBytes) { + $this->writeLogWithCrlf('[cutted:'.$iReadBytes.'] < '.\substr($sResponseBuffer, 0, $iLimit).'...'); + } else { + $this->writeLogWithCrlf('< '.$sResponseBuffer); } + + return $sResponseBuffer; } abstract function getLogName() : string; protected function writeLog(string $sDesc, int $iDescType = \LOG_INFO) : void { - $this->oLogger && $this->oLogger->Write($sDesc, $iDescType, $this->getLogName()); + $this->logWrite($sDesc, $iDescType, $this->getLogName()); } protected function writeLogWithCrlf(string $sDesc) : void { - $this->oLogger && $this->oLogger->Write($sDesc, \LOG_INFO, $this->getLogName(), true, true); + $this->logWrite($sDesc, \LOG_INFO, $this->getLogName(), true, true); } - protected function writeLogException(\Throwable $oException, - int $iDescType = \LOG_NOTICE, bool $bThrowException = false) : void + protected function writeLogException(\Throwable $oException, int $iDescType = \LOG_NOTICE, bool $bThrowException = true) : void { - if ($this->oLogger) - { - if ($oException instanceof Exceptions\SocketCanNotConnectToHostException) - { - $this->oLogger->Write('Socket: ['.$oException->getSocketCode().'] '.$oException->getSocketMessage(), $iDescType, $this->getLogName()); - } - - $this->oLogger->WriteException($oException, $iDescType, $this->getLogName()); + if ($oException instanceof Exceptions\SocketCanNotConnectToHostException) { + $this->logWrite('Socket: ['.$oException->getSocketCode().'] '.$oException->getSocketMessage(), $iDescType, $this->getLogName()); } - - if ($bThrowException) - { + $this->logException($oException, $iDescType, $this->getLogName()); + if ($bThrowException) { throw $oException; } } - - public function SetLogger(\MailSo\Log\Logger $oLogger) : void - { - $this->oLogger = $oLogger; - } - - public function Logger() : ?\MailSo\Log\Logger - { - return $this->oLogger; - } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Net/SSLContext.php b/snappymail/v/0.0.0/app/libraries/MailSo/Net/SSLContext.php new file mode 100644 index 000000000..e34972061 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Net/SSLContext.php @@ -0,0 +1,73 @@ +verify_peer = !!$oConfig->Get('ssl', 'verify_certificate', true); + $this->verify_peer_name = !!$oConfig->Get('ssl', 'verify_certificate', true); + $this->allow_self_signed = !!$oConfig->Get('ssl', 'allow_self_signed', false); + $this->cafile = \trim($oConfig->Get('ssl', 'cafile', '')); + $this->capath = \trim($oConfig->Get('ssl', 'capath', '')); + $this->disable_compression = !!$oConfig->Get('ssl', 'disable_compression', true); + $this->security_level = (int) $oConfig->Get('ssl', 'security_level', 1); + $this->local_cert = \trim($oConfig->Get('ssl', 'local_cert', '')); + } + + public static function fromArray(array $settings) : self + { + $object = new static; + foreach ($settings as $key => $value) { + $object->$key = $value; + } + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aResult = \get_object_vars($this); +// $aResult['@Object'] = 'Object/SSLContext'; + return \array_filter( + $aResult, + function($var){return !\is_string($var) || \strlen($var);} + ); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/ResponseException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/ResponseException.php index a6600d13f..15a194737 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/ResponseException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Exceptions/ResponseException.php @@ -18,10 +18,7 @@ namespace MailSo\Sieve\Exceptions; */ class ResponseException extends \MailSo\RuntimeException { - /** - * @var array - */ - private $aResponses; + private array $aResponses; public function __construct(array $aResponses = array(), string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null) { @@ -37,7 +34,7 @@ class ResponseException extends \MailSo\RuntimeException public function GetLastResponse() : ?\MailSo\Sieve\Response { - $iCnt = count($this->aResponses); + $iCnt = \count($this->aResponses); return $iCnt ? $this->aResponses[$iCnt - 1] : null; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Settings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Settings.php new file mode 100644 index 000000000..d59cb9b1d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/Settings.php @@ -0,0 +1,54 @@ +authPlainLiteral = !$oConfig->Get('labs', 'sieve_auth_plain_initial', true); + } + + public static function fromArray(array $aSettings) : self + { + $object = parent::fromArray($aSettings); + $object->enabled = !empty($aSettings['enabled']); +// $object->authPlainLiteral = !empty($aSettings['authPlainLiteral']); + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_merge( + parent::jsonSerialize(), + [ +// '@Object' => 'Object/SmtpSettings', + 'enabled' => $this->enabled, +// 'authPlainLiteral' => $this->authPlainLiteral + ] + ); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/ManageSieveClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/SieveClient.php similarity index 59% rename from snappymail/v/0.0.0/app/libraries/MailSo/Sieve/ManageSieveClient.php rename to snappymail/v/0.0.0/app/libraries/MailSo/Sieve/SieveClient.php index c67ca7c7a..7dadb322e 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/ManageSieveClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Sieve/SieveClient.php @@ -17,48 +17,26 @@ use MailSo\Net\Enumerations\ConnectionSecurityType; * @category MailSo * @package Sieve */ -class ManageSieveClient extends \MailSo\Net\NetClient +class SieveClient extends \MailSo\Net\NetClient { - /** - * @var bool - */ - private $bIsLoggined = false; + private bool $bIsLoggined = false; - /** - * @var array - */ - private $aCapa = array(); + private array $aCapa = array(); - /** - * @var array - */ - private $aAuth = array(); + private array $aAuth = array(); - /** - * @var array - */ - private $aModules = array(); + private array $aModules = array(); - public function IsSupported(string $sCapa) : bool + public function hasCapability(string $sCapa) : bool { return isset($this->aCapa[\strtoupper($sCapa)]); } - public function IsModuleSupported(string $sModule) : bool - { - return $this->IsSupported('SIEVE') && \in_array(\strtolower(\trim($sModule)), $this->aModules); - } - public function Modules() : array { return $this->aModules; } - public function IsAuthSupported(string $sAuth) : bool - { - return $this->IsSupported('SASL') && \in_array(\strtoupper($sAuth), $this->aAuth); - } - /** * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException @@ -70,80 +48,81 @@ class ManageSieveClient extends \MailSo\Net\NetClient parent::Connect($oSettings); $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); $this->parseStartupResponse($aResponse); - if (ConnectionSecurityType::STARTTLS === $this->iSecurityType - || (ConnectionSecurityType::AUTO_DETECT === $this->iSecurityType && $this->IsSupported('STARTTLS'))) { + if (ConnectionSecurityType::STARTTLS === $this->Settings->type + || (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) { $this->StartTLS(); } } private function StartTLS() : void { - if ($this->IsSupported('STARTTLS')) { + if ($this->hasCapability('STARTTLS')) { $this->sendRequestWithCheck('STARTTLS'); $this->EnableCrypto(); $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); $this->parseStartupResponse($aResponse); } else { $this->writeLogException( new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), - \LOG_ERR, true); + \LOG_ERR); } } + public function supportsAuthType(string $sasl_type) : bool + { + return \in_array(\strtoupper($sasl_type), $this->aAuth); + } + /** * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Sieve\Exceptions\* */ - public function Login(array $aCredentials) : self + public function Login(Settings $oSettings) : self { - $sLogin = $aCredentials['Login']; - $sPassword = $aCredentials['Password']; + if ($this->bIsLoggined) { + return $this; + } + + $sLogin = $oSettings->Login; + $sPassword = $oSettings->Password; $sLoginAuthKey = ''; - if (!\strlen($sLogin) || !\strlen($sPassword)) - { - $this->writeLogException( - new \InvalidArgumentException, - \LOG_ERR, true); + if (!\strlen($sLogin) || !\strlen($sPassword)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } $type = ''; - \array_push($aCredentials['SASLMechanisms'], 'PLAIN', 'LOGIN'); - foreach ($aCredentials['SASLMechanisms'] as $sasl_type) { - if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) { - $type = $sasl_type; - break; + if ($this->hasCapability('SASL')) { + foreach ($oSettings->SASLMechanisms as $sasl_type) { + if (\in_array(\strtoupper($sasl_type), $this->aAuth) && \SnappyMail\SASL::isSupported($sasl_type)) { + $type = $sasl_type; + break; + } } } - if (!$type) { - if (!$this->Encrypted() && $this->IsSupported('STARTTLS')) { + if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) { $this->StartTLS(); - return $this->Login($aCredentials); + return $this->Login($oSettings); } - $this->writeLogException( - new \MailSo\Sieve\Exceptions\LoginException, - \LOG_ERR, true); + $this->writeLogException(new \MailSo\Sieve\Exceptions\LoginException, \LOG_ERR); } $SASL = \SnappyMail\SASL::factory($type); - $SASL->base64 = true; $bAuth = false; try { - if (0 === \strpos($type, 'SCRAM-')) + if (\str_starts_with($type, 'SCRAM-')) { /* $sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), \MailSo\Imap\Enumerations\ResponseType::CONTINUATION); $this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid* /), true); $sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), \MailSo\Imap\Enumerations\ResponseType::CONTINUATION)); - $this->oLogger && $this->oLogger->AddSecret($sChallenge); + $this->logMask($sChallenge); $this->sendRaw($sChallenge); $oResponse = $this->getResponse(); $SASL->verify($this->getResponseValue($oResponse)); @@ -152,17 +131,16 @@ class ManageSieveClient extends \MailSo\Net\NetClient else if ('PLAIN' === $type || 'OAUTHBEARER' === $type || 'XOAUTH2' === $type) { $sAuth = $SASL->authenticate($sLogin, $sPassword, $sLoginAuthKey); - $this->oLogger && $this->oLogger->AddSecret($sAuth); + $this->logMask($sAuth); - if ($aCredentials['InitialAuthPlain']) { - $this->sendRaw("AUTHENTICATE \"{$type}\" \"{$sAuth}\""); - } else { + if ($oSettings->authPlainLiteral) { $this->sendRaw("AUTHENTICATE \"{$type}\" {".\strlen($sAuth).'+}'); $this->sendRaw($sAuth); + } else { + $this->sendRaw("AUTHENTICATE \"{$type}\" \"{$sAuth}\""); } $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); $this->parseStartupResponse($aResponse); $bAuth = true; } @@ -170,7 +148,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient { $sLogin = $SASL->authenticate($sLogin, $sPassword); $sPassword = $SASL->challenge(''); - $this->oLogger && $this->oLogger->AddSecret($sPassword); + $this->logMask($sPassword); $this->sendRaw('AUTHENTICATE "LOGIN"'); $this->sendRaw('{'.\strlen($sLogin).'+}'); @@ -179,7 +157,6 @@ class ManageSieveClient extends \MailSo\Net\NetClient $this->sendRaw($sPassword); $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); $this->parseStartupResponse($aResponse); $bAuth = true; } @@ -187,17 +164,11 @@ class ManageSieveClient extends \MailSo\Net\NetClient catch (\MailSo\Sieve\Exceptions\NegativeResponseException $oException) { $this->writeLogException( - new \MailSo\Sieve\Exceptions\LoginBadCredentialsException( - $oException->GetResponses(), '', 0, $oException), - \LOG_ERR, true); + new \MailSo\Sieve\Exceptions\LoginBadCredentialsException($oException->GetResponses(), '', 0, $oException), + \LOG_ERR); } - if (!$bAuth) - { - $this->writeLogException( - new \MailSo\Sieve\Exceptions\LoginBadMethodException, - \LOG_ERR, true); - } + $bAuth || $this->writeLogException(new \MailSo\Sieve\Exceptions\LoginBadMethodException, \LOG_ERR); $this->bIsLoggined = true; @@ -211,8 +182,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ public function Logout() : void { - if ($this->bIsLoggined) - { + if ($this->bIsLoggined) { $this->sendRequestWithCheck('LOGOUT'); $this->bIsLoggined = false; } @@ -227,18 +197,12 @@ class ManageSieveClient extends \MailSo\Net\NetClient { $this->sendRequest('LISTSCRIPTS'); $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); - $aResult = array(); - foreach ($aResponse as $sLine) - { + foreach ($aResponse as $sLine) { $aTokens = $this->parseLine($sLine); - if (!$aTokens) - { - continue; + if ($aTokens) { + $aResult[$aTokens[0]] = 'ACTIVE' === \substr($sLine, -6); } - - $aResult[$aTokens[0]] = 'ACTIVE' === substr($sLine, -6); } return $aResult; @@ -253,7 +217,6 @@ class ManageSieveClient extends \MailSo\Net\NetClient { $this->sendRequest('CAPABILITY'); $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); $this->parseStartupResponse($aResponse); return $this->aCapa; @@ -278,20 +241,17 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ public function GetScript(string $sScriptName) : string { + $sScriptName = \addcslashes($sScriptName, '"\\'); $this->sendRequest('GETSCRIPT "'.$sScriptName.'"'); $aResponse = $this->parseResponse(); - $this->validateResponse($aResponse); $sScript = ''; - if (\count($aResponse)) - { - if ('{' === $aResponse[0][0]) - { + if (\count($aResponse)) { + if ('{' === $aResponse[0][0]) { \array_shift($aResponse); } - if (\in_array(\substr($aResponse[\count($aResponse) - 1], 0, 2), array('OK', 'NO'))) - { + if (\in_array(\substr($aResponse[\count($aResponse) - 1], 0, 2), array('OK', 'NO'))) { \array_pop($aResponse); } @@ -308,6 +268,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ public function PutScript(string $sScriptName, string $sScriptSource) : self { + $sScriptName = \addcslashes($sScriptName, '"\\'); $sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource); $this->sendRequest('PUTSCRIPT "'.$sScriptName.'" {'.\strlen($sScriptSource).'+}'); $this->sendRequestWithCheck($sScriptSource); @@ -336,6 +297,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ public function SetActiveScript(string $sScriptName) : self { + $sScriptName = \addcslashes($sScriptName, '"\\'); $this->sendRequestWithCheck('SETACTIVE "'.$sScriptName.'"'); return $this; @@ -348,6 +310,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ public function DeleteScript(string $sScriptName) : self { + $sScriptName = \addcslashes($sScriptName, '"\\'); $this->sendRequestWithCheck('DELETESCRIPT "'.$sScriptName.'"'); return $this; @@ -360,68 +323,22 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ public function RenameScript(string $sOldName, string $sNewName) : self { + $sOldName = \addcslashes($sOldName, '"\\'); + $sNewName = \addcslashes($sNewName, '"\\'); $this->sendRequestWithCheck('RENAMESCRIPT "'.$sOldName.'" "'.$sNewName.'"'); return $this; } - /** - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - * @throws \MailSo\Sieve\Exceptions\* - */ - public function GetActiveScriptName() : string - { - $aList = $this->ListScripts(); - foreach ($aList as $sName => $bIsActive) - { - if ($bIsActive) - { - return $sName; - } - } - - return ''; - } - - /** - * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\Exceptions\* - * @throws \MailSo\Sieve\Exceptions\* - */ - public function IsActiveScript(string $sScriptName) : bool - { - return $sScriptName === $this->GetActiveScriptName(); - } - private function parseLine(string $sLine) : ?array { - if (false === $sLine || null === $sLine || \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) - { - return null; - } - - $iStart = -1; - $iIndex = 0; - $aResult = array(); - - for ($iPos = 0; $iPos < \strlen($sLine); $iPos++) - { - if ('"' === $sLine[$iPos] && '\\' !== $sLine[$iPos]) - { - if (-1 === $iStart) - { - $iStart = $iPos; - } - else - { - $aResult[$iIndex++] = \substr($sLine, $iStart + 1, $iPos - $iStart - 1); - $iStart = -1; - } + if (!\in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) { + $aResult = array(); + if (\preg_match_all('/(?:(?:"((?:\\\\"|[^"])*)"))/', $sLine, $aResult)) { + return \array_map('stripcslashes', $aResult[1]); } } - - return isset($aResult[0]) ? $aResult : null; + return null; } /** @@ -431,21 +348,16 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ private function parseStartupResponse(array $aResponse) : void { - foreach ($aResponse as $sLine) - { + foreach ($aResponse as $sLine) { $aTokens = $this->parseLine($sLine); - - if (false === $aTokens || !isset($aTokens[0]) || - \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) - { + if (empty($aTokens[0]) || \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) { continue; } $sToken = \strtoupper($aTokens[0]); $this->aCapa[$sToken] = isset($aTokens[1]) ? $aTokens[1] : ''; - if (isset($aTokens[1])) - { + if (isset($aTokens[1])) { switch ($sToken) { case 'SASL': $this->aAuth = \explode(' ', \strtoupper($aTokens[1])); @@ -465,11 +377,8 @@ class ManageSieveClient extends \MailSo\Net\NetClient */ private function sendRequest(string $sRequest) : void { - if (!\strlen(\trim($sRequest))) - { - $this->writeLogException( - new \InvalidArgumentException, - \LOG_ERR, true); + if (!\strlen(\trim($sRequest))) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } $this->IsConnected(true); @@ -485,76 +394,45 @@ class ManageSieveClient extends \MailSo\Net\NetClient private function sendRequestWithCheck(string $sRequest) : void { $this->sendRequest($sRequest); - $this->validateResponse($this->parseResponse()); - } - - private function convertEndOfLine(string $sLine) : string - { - $sLine = \trim($sLine); - if ('}' === \substr($sLine, -1)) - { - $iPos = \strrpos($sLine, '{'); - if (false !== $iPos) - { - $sSunLine = \substr($sLine, $iPos + 1, -1); - if (\is_numeric($sSunLine) && 0 < (int) $sSunLine) - { - $iLen = (int) $sSunLine; - - $this->getNextBuffer($iLen, true); - - if (\strlen($this->sResponseBuffer) === $iLen) - { - $sLine = \trim(\substr_replace($sLine, $this->sResponseBuffer, $iPos)); - } - } - } - } - - return $sLine; + $this->parseResponse(); } private function parseResponse() : array { $aResult = array(); - do - { - $this->getNextBuffer(); - - $sLine = $this->sResponseBuffer; - if (false === $sLine) - { + while (true) { + $sResponseBuffer = $this->getNextBuffer(); + if (null === $sResponseBuffer) { break; } - else if (\in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) - { - $aResult[] = $this->convertEndOfLine($sLine); - break; + $bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO')); + // convertEndOfLine + $sLine = \trim($sResponseBuffer); + if ('}' === \substr($sLine, -1)) { + $iPos = \strrpos($sLine, '{'); + if (false !== $iPos) { + $iLen = \intval(\substr($sLine, $iPos + 1, -1)); + if (0 < $iLen) { + $sResponseBuffer = $this->getNextBuffer($iLen); + if (\strlen($sResponseBuffer) === $iLen) { + $sLine = \trim(\substr_replace($sLine, $sResponseBuffer, $iPos)); + } + } + } } - else - { - $aResult[] = $this->convertEndOfLine($sLine); + $aResult[] = $sLine; + if ($bEnd) { + break; } } - while (true); + + if (!$aResult || 'OK' !== \substr($aResult[\array_key_last($aResult)], 0, 2)) { + $this->writeLogException(new \MailSo\Sieve\Exceptions\NegativeResponseException($aResult), \LOG_WARNING); + } return $aResult; } - /** - * @throws \MailSo\Sieve\Exceptions\NegativeResponseException - */ - private function validateResponse(array $aResponse) - { - if (!$aResponse || - 'OK' !== \substr($aResponse[\count($aResponse) - 1], 0, 2)) - { - $this->writeLogException( - new \MailSo\Sieve\Exceptions\NegativeResponseException($aResponse), - \LOG_WARNING, true); - } - } - public function getLogName() : string { return 'SIEVE'; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/ResponseException.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/ResponseException.php index 79d9be1cc..8de5405f1 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/ResponseException.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Exceptions/ResponseException.php @@ -18,10 +18,7 @@ namespace MailSo\Smtp\Exceptions; */ class ResponseException extends \MailSo\RuntimeException { - /** - * @var array - */ - private $aResponses; + private array $aResponses; public function __construct(array $aResponses = array(), string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null) { @@ -37,7 +34,7 @@ class ResponseException extends \MailSo\RuntimeException public function GetLastResponse() : ?\MailSo\Smtp\Response { - $iCnt = count($this->aResponses); + $iCnt = \count($this->aResponses); return $iCnt ? $this->aResponses[$iCnt - 1] : null; } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Settings.php b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Settings.php new file mode 100644 index 000000000..de25b99cd --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Smtp/Settings.php @@ -0,0 +1,67 @@ +viewErrors = !!$oConfig->Get('labs', 'smtp_show_server_errors', false); + } + + public static function fromArray(array $aSettings) : self + { + $object = parent::fromArray($aSettings); + $object->useAuth = !empty($aSettings['useAuth']); + $object->setSender = !empty($aSettings['setSender']); + $object->usePhpMail = !empty($aSettings['usePhpMail']); + $object->authPlainLine = !empty($aSettings['authPlainLine']); +// $object->viewErrors = !empty($aSettings['viewErrors']); + return $object; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return \array_merge( + parent::jsonSerialize(), + [ +// '@Object' => 'Object/SmtpSettings', + 'useAuth' => $this->useAuth, + 'setSender' => $this->setSender, + 'usePhpMail' => $this->usePhpMail, + 'authPlainLine' => $this->authPlainLine +// 'viewErrors' => $this->viewErrors + ] + ); + } + +} 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 53d162ee6..6ca0fc84f 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 @@ -19,78 +19,49 @@ use MailSo\Net\Enumerations\ConnectionSecurityType; */ class SmtpClient extends \MailSo\Net\NetClient { - /** - * @var string - */ - private $sEhlo = ''; + private bool $bIsLoggined = false; + + private string $sEhlo = ''; + + private bool $bRcpt = false; + + private bool $bMail = false; + + private array $aAuthTypes = array(); + + private array $aCapa = array(); /** - * @var bool + * RFC 1870 */ - private $bRcpt = false; + private int $iSizeCapaValue = 0; - /** - * @var bool - */ - private $bMail = false; + private array $aResults = array(); - /** - * @var bool - */ - private $bData = false; - - /** - * @var array - */ - private $aAuthTypes = array(); - - /** - * @var array - */ - private $aCapa = array(); - - /** - * @var int - */ - private $iSizeCapaValue = 0; - - /** - * @var array - */ - private $aResults = array(); - - public function IsSupported(string $sCapa) : bool + public function hasCapability(string $sCapa) : bool { return \in_array(\strtoupper($sCapa), $this->aCapa); } - public function IsAuthSupported(string $sAuth) : bool + public function maxSize() : int { - return \in_array(\strtoupper($sAuth), $this->aAuthTypes); - } - - public function HasSupportedAuth() : bool - { - return $this->IsAuthSupported('PLAIN') || $this->IsAuthSupported('LOGIN'); + return $this->iSizeCapaValue; } public static function EhloHelper() : string { $sEhloHost = empty($_SERVER['SERVER_NAME']) ? '' : \trim($_SERVER['SERVER_NAME']); - if (empty($sEhloHost)) - { + if (empty($sEhloHost)) { $sEhloHost = empty($_SERVER['HTTP_HOST']) ? '' : \trim($_SERVER['HTTP_HOST']); } - if (empty($sEhloHost)) - { + if (empty($sEhloHost)) { $sEhloHost = \function_exists('gethostname') ? \gethostname() : 'localhost'; } $sEhloHost = \trim(\preg_replace('/:\d+$/', '', \trim($sEhloHost))); - if (\preg_match('/^\d+\.\d+\.\d+\.\d+$/', $sEhloHost)) - { + if (\preg_match('/^\d+\.\d+\.\d+\.\d+$/', $sEhloHost)) { $sEhloHost = '['.$sEhloHost.']'; } @@ -103,127 +74,140 @@ class SmtpClient extends \MailSo\Net\NetClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Smtp\Exceptions\* */ - public function Connect(\MailSo\Net\ConnectSettings $oSettings, string $sEhloHost = '[127.0.0.1]') : void + public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void { parent::Connect($oSettings); $this->validateResponse(220); - $this->ehloOrHelo($sEhloHost); - $this->sEhlo = $sEhloHost; + $this->ehloOrHelo($oSettings->Ehlo); + $this->sEhlo = $oSettings->Ehlo; - if (ConnectionSecurityType::STARTTLS === $this->iSecurityType - || (ConnectionSecurityType::AUTO_DETECT === $this->iSecurityType && $this->IsSupported('STARTTLS'))) { + if (ConnectionSecurityType::STARTTLS === $this->Settings->type + || (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) { $this->StartTLS(); } } private function StartTLS() : void { - if ($this->IsSupported('STARTTLS')) { + if ($this->hasCapability('STARTTLS')) { $this->sendRequestWithCheck('STARTTLS', 220); $this->EnableCrypto(); $this->ehloOrHelo($this->sEhlo); } else { $this->writeLogException( new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'), - \LOG_ERR, true); + \LOG_ERR); } } + public function supportsAuthType(string $sasl_type) : bool + { + return \in_array(\strtoupper($sasl_type), $this->aAuthTypes); + } + /** * @throws \InvalidArgumentException * @throws \MailSo\RuntimeException - * @throws \MailSo\Net\* + * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Smtp\Exceptions\* */ - public function Login(array $aCredentials) : self + public function Login(Settings $oSettings) : self { - $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login'])); - $sPassword = $aCredentials['Password']; + if ($this->bIsLoggined) { + return $this; + } + + $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($oSettings->Login)); + $sPassword = $oSettings->Password; $type = ''; - // https://github.com/the-djmaze/snappymail/pull/423 -// $aCredentials['SASLMechanisms'][] = 'LOGIN'; - \array_unshift($aCredentials['SASLMechanisms'], 'LOGIN'); - foreach ($aCredentials['SASLMechanisms'] as $sasl_type) { - if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) { + foreach ($oSettings->SASLMechanisms as $sasl_type) { + if (\in_array(\strtoupper($sasl_type), $this->aAuthTypes) && \SnappyMail\SASL::isSupported($sasl_type)) { $type = $sasl_type; break; } } - if (!$type) { - if (!$this->Encrypted() && $this->IsSupported('STARTTLS')) { + if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) { $this->StartTLS(); - return $this->Login($aCredentials); + return $this->Login($oSettings); } \trigger_error("SMTP {$this->GetConnectedHost()} no supported AUTH options. Disable login"); - $this->writeLogException( - new \MailSo\Smtp\Exceptions\LoginBadMethodException, - \LOG_NOTICE, true); + $this->writeLogException(new \MailSo\Smtp\Exceptions\LoginBadMethodException); } $SASL = \SnappyMail\SASL::factory($type); - $SASL->base64 = true; - // Start authentication - try - { - $sResult = $this->sendRequestWithCheck('AUTH', 334, $type); - } - catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) - { - $this->writeLogException( - new \MailSo\Smtp\Exceptions\LoginBadMethodException( - $oException->GetResponses(), $oException->getMessage(), 0, $oException), - \LOG_NOTICE, true); - } + if ($this->Settings->authPlainLine && $SASL instanceof \SnappyMail\SASL\Plain) { + // https://github.com/the-djmaze/snappymail/issues/1038 + try + { + $sResult = $this->sendRequestWithCheck('AUTH', 235, 'PLAIN ' . $SASL->authenticate($sLogin, $sPassword)); + } + catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Smtp\Exceptions\LoginBadCredentialsException($oException->GetResponses(), $oException->getMessage(), 0, $oException) + ); + } + } else { + // Start authentication + try + { + $sResult = $this->sendRequestWithCheck('AUTH', 334, $type); + } + catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Smtp\Exceptions\LoginBadMethodException($oException->GetResponses(), $oException->getMessage(), 0, $oException) + ); + } - try - { - if (0 === \strpos($type, 'SCRAM-')) { - // RFC 5802 - $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 234, ''); - $sChallenge = $SASL->challenge($sResult); - $this->oLogger && $this->oLogger->AddSecret($sChallenge); - $SASL->verify($this->sendRequestWithCheck($sChallenge, 235, '', true)); - } else switch ($type) { - // RFC 4616 - case 'PLAIN': - case 'XOAUTH2': - case 'OAUTHBEARER': - $sAuth = $SASL->authenticate($sLogin, $sPassword); - $this->oLogger && $this->oLogger->AddSecret($sAuth); - $this->sendRequestWithCheck($sAuth, 235, '', true); - break; + try + { + if (\str_starts_with($type, 'SCRAM-')) { + // RFC 5802 + $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 234, ''); + $sChallenge = $SASL->challenge($sResult); + $this->logMask($sChallenge); + $SASL->verify($this->sendRequestWithCheck($sChallenge, 235, '', true)); + } else switch ($type) { + // RFC 4616 + case 'PLAIN': + case 'XOAUTH2': + case 'OAUTHBEARER': + $sAuth = $SASL->authenticate($sLogin, $sPassword); + $this->logMask($sAuth); + $this->sendRequestWithCheck($sAuth, 235, '', true); + break; - case 'LOGIN': - $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 334, ''); - $sPassword = $SASL->challenge($sResult); - $this->oLogger && $this->oLogger->AddSecret($sPassword); - $this->sendRequestWithCheck($sPassword, 235, '', true); - break; + case 'LOGIN': + $sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 334, ''); + $sPassword = $SASL->challenge($sResult); + $this->logMask($sPassword); + $this->sendRequestWithCheck($sPassword, 235, '', true); + break; - // RFC 2195 - case 'CRAM-MD5': - if (empty($sResult)) { - $this->writeLogException( - new \MailSo\Smtp\Exceptions\NegativeResponseException, - \LOG_NOTICE, true - ); + // RFC 2195 + case 'CRAM-MD5': + if (empty($sResult)) { + $this->writeLogException(new \MailSo\Smtp\Exceptions\NegativeResponseException); + } + $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, '', true); + break; } - $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, '', true); - break; + } + catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) + { + $this->writeLogException( + new \MailSo\Smtp\Exceptions\LoginBadCredentialsException($oException->GetResponses(), $oException->getMessage(), 0, $oException) + ); } } - catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) - { - $this->writeLogException( - new \MailSo\Smtp\Exceptions\LoginBadCredentialsException( - $oException->GetResponses(), $oException->getMessage(), 0, $oException), - \LOG_NOTICE, true); - } + + $this->bIsLoggined = true; return $this; } @@ -233,28 +217,35 @@ class SmtpClient extends \MailSo\Net\NetClient * @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Smtp\Exceptions\* */ - public function MailFrom(string $sFrom, string $sSizeIfSupported = '', bool $bDsn = false) : self + public function MailFrom(string $sFrom, int $iSizeIfSupported = 0, bool $bDsn = false, bool $bRequireTLS = false) : self { - $sFrom = \MailSo\Base\Utils::IdnToAscii( - \MailSo\Base\Utils::Trim($sFrom), true); + $sFrom = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sFrom), true); - $sCmd = 'FROM:<'.$sFrom.'>'; + $sCmd = "FROM:<{$sFrom}>"; - if (\strlen($sSizeIfSupported) && \is_numeric($sSizeIfSupported) && $this->IsSupported('SIZE')) - { - $sCmd .= ' SIZE='.$sSizeIfSupported; + if (0 < $iSizeIfSupported && $this->hasCapability('SIZE')) { + $sCmd .= ' SIZE='.$iSizeIfSupported; } - if ($bDsn && $this->IsSupported('DSN')) - { + // RFC 3461 + if ($bDsn && $this->hasCapability('DSN')) { $sCmd .= ' RET=HDRS'; } + // RFC 6152 + if ($this->hasCapability('8BITMIME')) { +// $sCmd .= ' BODY=8BITMIME'; + } + + // RFC 8689 + if ($bRequireTLS && $this->hasCapability('REQUIRETLS')) { + $sCmd .= ' REQUIRETLS'; + } + $this->sendRequestWithCheck('MAIL', 250, $sCmd); $this->bMail = true; $this->bRcpt = false; - $this->bData = false; return $this; } @@ -266,20 +257,15 @@ class SmtpClient extends \MailSo\Net\NetClient */ public function Rcpt(string $sTo, bool $bDsn = false) : self { - if (!$this->bMail) - { - $this->writeLogException( - new \MailSo\RuntimeException('No sender reverse path has been supplied'), - \LOG_ERR, true); + if (!$this->bMail) { + $this->writeLogException(new \MailSo\RuntimeException('No sender reverse path has been supplied'), \LOG_ERR); } - $sTo = \MailSo\Base\Utils::IdnToAscii( - \MailSo\Base\Utils::Trim($sTo), true); + $sTo = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sTo), true); $sCmd = 'TO:<'.$sTo.'>'; - if ($bDsn && $this->IsSupported('DSN')) - { + if ($bDsn && $this->hasCapability('DSN')) { $sCmd .= ' NOTIFY=SUCCESS,FAILURE'; } @@ -310,8 +296,7 @@ class SmtpClient extends \MailSo\Net\NetClient */ public function Data(string $sData) : self { - if (!\strlen(\trim($sData))) - { + if (!\strlen(\trim($sData))) { throw new \InvalidArgumentException; } @@ -333,16 +318,12 @@ class SmtpClient extends \MailSo\Net\NetClient */ public function DataWithStream($rDataStream) : self { - if (!\is_resource($rDataStream)) - { + if (!\is_resource($rDataStream)) { throw new \InvalidArgumentException; } - if (!$this->bRcpt) - { - $this->writeLogException( - new \MailSo\RuntimeException('No recipient forward path has been supplied'), - \LOG_ERR, true); + if (!$this->bRcpt) { + $this->writeLogException(new \MailSo\RuntimeException('No recipient forward path has been supplied'), \LOG_ERR); } $this->sendRequestWithCheck('DATA', 354); @@ -351,37 +332,25 @@ class SmtpClient extends \MailSo\Net\NetClient $this->bRunningCallback = true; - while (!\feof($rDataStream)) - { + while (!\feof($rDataStream)) { $sBuffer = \fgets($rDataStream); - if (false !== $sBuffer) - { - if (0 === \strpos($sBuffer, '.')) - { - $sBuffer = '.'.$sBuffer; + if (false === $sBuffer) { + if (!\feof($rDataStream)) { + $this->writeLogException(new \MailSo\RuntimeException('Cannot read input resource'), \LOG_ERR); } - - $this->sendRaw(\rtrim($sBuffer, "\r\n"), false); - - \MailSo\Base\Utils::ResetTimeLimit(); - continue; + break; } - else if (!\feof($rDataStream)) - { - $this->writeLogException( - new \MailSo\RuntimeException('Cannot read input resource'), - \LOG_ERR, true); + if (\str_starts_with($sBuffer, '.')) { + $sBuffer = '.' . $sBuffer; } - - break; + $this->sendRaw(\rtrim($sBuffer, "\r\n"), false); + \MailSo\Base\Utils::ResetTimeLimit(); } $this->sendRequestWithCheck('.', 250); $this->bRunningCallback = false; - $this->bData = true; - return $this; } @@ -396,7 +365,6 @@ class SmtpClient extends \MailSo\Net\NetClient $this->bMail = false; $this->bRcpt = false; - $this->bData = false; return $this; } @@ -408,11 +376,8 @@ class SmtpClient extends \MailSo\Net\NetClient */ public function Vrfy(string $sUser) : self { - $sUser = \MailSo\Base\Utils::IdnToAscii( - \MailSo\Base\Utils::Trim($sUser)); - - $this->sendRequestWithCheck('VRFY', array(250, 251, 252), $sUser); - + $this->sendRequestWithCheck('VRFY', array(250, 251, 252), + \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser))); return $this; } @@ -435,14 +400,11 @@ class SmtpClient extends \MailSo\Net\NetClient */ public function Logout() : void { - if ($this->IsConnected()) - { + if ($this->IsConnected()) { $this->sendRequestWithCheck('QUIT', 221); } - $this->bMail = false; $this->bRcpt = false; - $this->bData = false; } /** @@ -452,19 +414,16 @@ class SmtpClient extends \MailSo\Net\NetClient */ private function sendRequest(string $sCommand, string $sAddToCommand = '', bool $bSecureLog = false) : void { - if (!\strlen(\trim($sCommand))) - { - $this->writeLogException( - new \InvalidArgumentException, - \LOG_ERR, true); + $sCommand = \trim($sCommand); + if (!\strlen($sCommand)) { + $this->writeLogException(new \InvalidArgumentException, \LOG_ERR); } $this->IsConnected(true); - $sCommand = \trim($sCommand); - $sRealCommand = $sCommand.(0 === \strlen($sAddToCommand) ? '' : ' '.$sAddToCommand); + $sRealCommand = $sCommand . (\strlen($sAddToCommand) ? ' '.$sAddToCommand : ''); - $sFakeCommand = ($bSecureLog) ? '********' : ''; + $sFakeCommand = $bSecureLog ? '********' : ''; $this->sendRaw($sRealCommand, true, $sFakeCommand); } @@ -509,33 +468,32 @@ class SmtpClient extends \MailSo\Net\NetClient private function ehlo(string $sHost) : void { $this->sendRequestWithCheck('EHLO', 250, $sHost); - - foreach ($this->aResults as $sLine) - { + /* + 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 + */ + foreach ($this->aResults as $sLine) { $aMatch = array(); - if (\preg_match('/[\d]+[ \-](.+)$/', $sLine, $aMatch) && isset($aMatch[1]) && \strlen($aMatch[1])) - { - $sLine = \trim($aMatch[1]); - $aLine = \preg_split('/[ =]/', $sLine, 2); - if (!empty($aLine[0])) - { + if (\preg_match('/[\d]+[ \-](.+)$/', $sLine, $aMatch) && isset($aMatch[1]) && \strlen($aMatch[1])) { + $aLine = \preg_split('/[ =]/', \trim($aMatch[1]), 2); + if (!empty($aLine[0])) { $sCapa = \strtoupper($aLine[0]); - if (('AUTH' === $sCapa || 'SIZE' === $sCapa) && !empty($aLine[1])) - { + if (!empty($aLine[1]) && ('AUTH' === $sCapa || 'SIZE' === $sCapa)) { $sSubLine = \trim(\strtoupper($aLine[1])); - if (\strlen($sSubLine)) - { - if ('AUTH' === $sCapa) - { + if (\strlen($sSubLine)) { + if ('AUTH' === $sCapa) { $this->aAuthTypes = \explode(' ', $sSubLine); - } - else if ('SIZE' === $sCapa && \is_numeric($sSubLine)) - { + } else if ('SIZE' === $sCapa && \is_numeric($sSubLine)) { $this->iSizeCapaValue = (int) $sSubLine; } } } - $this->aCapa[] = $sCapa; } } @@ -560,43 +518,34 @@ class SmtpClient extends \MailSo\Net\NetClient */ private function validateResponse($mExpectCode, string $sErrorPrefix = '') : void { - if (!\is_array($mExpectCode)) - { - $mExpectCode = array((int) $mExpectCode); - } - else - { - $mExpectCode = \array_map('intval', $mExpectCode); - } + $mExpectCode = \is_array($mExpectCode) + ? \array_map('intval', $mExpectCode) + : array((int) $mExpectCode); $aParts = array('', '', ''); $this->aResults = array(); do { - $this->getNextBuffer(); - $aParts = \preg_split('/([\s\-]+)/', $this->sResponseBuffer, 2, PREG_SPLIT_DELIM_CAPTURE); + $sResponse = $this->getNextBuffer(); + $aParts = \preg_split('/([\s\-]+)/', $sResponse, 2, PREG_SPLIT_DELIM_CAPTURE); - if (3 === \count($aParts) && \is_numeric($aParts[0])) - { - if ('-' !== \substr($aParts[1], 0, 1) && !\in_array((int) $aParts[0], $mExpectCode)) - { + if (3 === \count($aParts) && \is_numeric($aParts[0])) { + if ('-' !== \substr($aParts[1], 0, 1) && !\in_array((int) $aParts[0], $mExpectCode)) { $this->writeLogException( new Exceptions\NegativeResponseException($this->aResults, ('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim( (\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : ''). - $this->sResponseBuffer)), \LOG_ERR, true); + $sResponse)), \LOG_ERR); } - } - else - { + } else { $this->writeLogException( new Exceptions\ResponseException($this->aResults, ('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim( (\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : ''). - $this->sResponseBuffer)), \LOG_ERR, true); + $sResponse)), \LOG_ERR); } - $this->aResults[] = $this->sResponseBuffer; + $this->aResults[] = $sResponse; } while ('-' === \substr($aParts[1], 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 0f7884f17..eb7356db5 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -2,8 +2,6 @@ namespace RainLoop; -use RainLoop\Enumerations\UploadError; - class Actions { use Actions\Admin; @@ -14,6 +12,8 @@ class Actions use Actions\Localization; use Actions\Themes; + use \MailSo\Log\Inherit; + const AUTH_MAILTO_TOKEN_KEY = 'smmailtoauth'; /** @@ -26,16 +26,17 @@ class Actions /** * This session cookie contains a \RainLoop\Model\Account + * Value is Base64 EncryptToJSON */ const AUTH_SPEC_TOKEN_KEY = 'smaccount'; /** * This session cookie optionally contains a \RainLoop\Model\AdditionalAccount + * Value is Base64 EncryptToJSON */ const AUTH_ADDITIONAL_TOKEN_KEY = 'smadditional'; - const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'smspeclogout'; - const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk'; + const APP_DUMMY = '********'; /** * @var \MailSo\Base\Http @@ -57,11 +58,6 @@ class Actions */ protected $oPlugins = null; - /** - * @var \MailSo\Log\Logger - */ - protected $oLogger = null; - /** * @var \MailSo\Log\Logger */ @@ -128,9 +124,12 @@ class Actions $sLogFileName = $this->oConfig->Get('logs', 'filename', ''); if ('syslog' === $sLogFileName) { $oDriver = new \MailSo\Log\Drivers\Syslog(); + } else if ('stderr' === $sLogFileName) { + $oDriver = new \MailSo\Log\Drivers\StderrStream(); } else { - $sLogFileFullPath = \APP_PRIVATE_DATA . 'logs/' . $this->compileLogFileName($sLogFileName); - $oDriver = new \MailSo\Log\Drivers\File($sLogFileFullPath); + $sLogFileFullPath = \trim($this->oConfig->Get('logs', 'path', '')) ?: \APP_PRIVATE_DATA . 'logs'; + \is_dir($sLogFileFullPath) || \mkdir($sLogFileFullPath, 0700, true); + $oDriver = new \MailSo\Log\Drivers\File($sLogFileFullPath . '/' . $this->compileLogFileName($sLogFileName)); } $this->oLogger->append($oDriver ->SetTimeZone($this->oConfig->Get('logs', 'time_zone', 'UTC')) @@ -138,14 +137,14 @@ class Actions $oHttp = $this->Http(); - $this->oLogger->Write( + $this->logWrite( '[SM:' . APP_VERSION . '][IP:' . $oHttp->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)) . '][PID:' . (\MailSo\Base\Utils::FunctionCallable('getmypid') ? \getmypid() : 'unknown') - . '][' . $oHttp->GetServer('SERVER_SOFTWARE', '~') + . '][' . \MailSo\Base\Http::GetServer('SERVER_SOFTWARE', '~') . '][' . (\MailSo\Base\Utils::FunctionCallable('php_sapi_name') ? \php_sapi_name() : '~') . '][Streams:' . \implode(',', \stream_get_transports()) - . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false, false) . $oHttp->GetServer('REQUEST_URI', '') . ']' + . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false) . \MailSo\Base\Http::GetServer('REQUEST_URI', '') . ']' ); } @@ -243,22 +242,22 @@ class Actions public function BootEnd(): void { try { - if ($this->MailClient()->IsLoggined()) { - $this->MailClient()->Disconnect(); + if ($this->ImapClient()->IsLoggined()) { + $this->ImapClient()->Disconnect(); } } catch (\Throwable $oException) { unset($oException); } } - protected function compileLogParams(string $sLine, ?Model\Account $oAccount = null, bool $bUrlEncode = false, array $aAdditionalParams = array()): string + protected function compileLogParams(string $sLine, ?Model\Account $oAccount = null, array $aAdditionalParams = array()): string { $aClear = array(); if (false !== \strpos($sLine, '{date:')) { $oConfig = $this->oConfig; - $sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($oConfig, $bUrlEncode) { - return Utils::UrlEncode((new \DateTime('now', new \DateTimeZone($oConfig->Get('logs', 'time_zone', 'UTC'))))->format($aMatch[1]), $bUrlEncode); + $sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($oConfig) { + return (new \DateTime('now', new \DateTimeZone($oConfig->Get('logs', 'time_zone', 'UTC'))))->format($aMatch[1]); }, $sLine); $aClear['/\{date:([^}]*)\}/'] = 'date'; @@ -270,13 +269,13 @@ class Actions } if ($oAccount) { - $sLine = \str_replace('{imap:login}', Utils::UrlEncode($oAccount->IncLogin(), $bUrlEncode), $sLine); - $sLine = \str_replace('{imap:host}', Utils::UrlEncode($oAccount->Domain()->IncHost(), $bUrlEncode), $sLine); - $sLine = \str_replace('{imap:port}', Utils::UrlEncode($oAccount->Domain()->IncPort(), $bUrlEncode), $sLine); + $sLine = \str_replace('{imap:login}', $oAccount->IncLogin(), $sLine); + $sLine = \str_replace('{imap:host}', $oAccount->Domain()->IncHost(), $sLine); + $sLine = \str_replace('{imap:port}', $oAccount->Domain()->IncPort(), $sLine); - $sLine = \str_replace('{smtp:login}', Utils::UrlEncode($oAccount->OutLogin(), $bUrlEncode), $sLine); - $sLine = \str_replace('{smtp:host}', Utils::UrlEncode($oAccount->Domain()->OutHost(), $bUrlEncode), $sLine); - $sLine = \str_replace('{smtp:port}', Utils::UrlEncode($oAccount->Domain()->OutPort(), $bUrlEncode), $sLine); + $sLine = \str_replace('{smtp:login}', $oAccount->OutLogin(), $sLine); + $sLine = \str_replace('{smtp:host}', $oAccount->Domain()->OutHost(), $sLine); + $sLine = \str_replace('{smtp:port}', $oAccount->Domain()->OutPort(), $sLine); } $aClear['/\{imap:([^}]*)\}/i'] = 'imap'; @@ -285,19 +284,19 @@ class Actions if (false !== \strpos($sLine, '{request:')) { if (false !== \strpos($sLine, '{request:ip}')) { - $sLine = \str_replace('{request:ip}', Utils::UrlEncode($this->Http()->GetClientIp( - $this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), $bUrlEncode), $sLine); + $sLine = \str_replace('{request:ip}', + $this->Http()->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), + $sLine); } if (false !== \strpos($sLine, '{request:domain}')) { - $sLine = \str_replace('{request:domain}', - Utils::UrlEncode($this->Http()->GetHost(false, true, true), $bUrlEncode), $sLine); + $sLine = \str_replace('{request:domain}', $this->Http()->GetHost(true, true), $sLine); } if (false !== \strpos($sLine, '{request:domain-clear}')) { $sLine = \str_replace('{request:domain-clear}', - Utils::UrlEncode( - \MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(false, true, true)), $bUrlEncode), $sLine); + \MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(true, true)), + $sLine); } $aClear['/\{request:([^}]*)\}/i'] = 'request'; @@ -306,15 +305,15 @@ class Actions if (false !== \strpos($sLine, '{user:')) { if (false !== \strpos($sLine, '{user:uid}')) { $sLine = \str_replace('{user:uid}', - Utils::UrlEncode(\base_convert(\sprintf('%u', - \crc32(Utils::GetConnectionToken())), 10, 32), $bUrlEncode), + \base_convert(\sprintf('%u', \crc32(Utils::GetConnectionToken())), 10, 32), $sLine ); } if (false !== \strpos($sLine, '{user:ip}')) { - $sLine = \str_replace('{user:ip}', Utils::UrlEncode($this->Http()->GetClientIp( - $this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), $bUrlEncode), $sLine); + $sLine = \str_replace('{user:ip}', + $this->Http()->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), + $sLine); } if (\preg_match('/\{user:(email|login|domain)\}/i', $sLine)) { @@ -325,14 +324,12 @@ class Actions if ($oAccount) { $sEmail = $oAccount->Email(); - $sLine = \str_replace('{user:email}', Utils::UrlEncode($sEmail, $bUrlEncode), $sLine); - $sLine = \str_replace('{user:login}', Utils::UrlEncode( - \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $bUrlEncode), $sLine); - $sLine = \str_replace('{user:domain}', Utils::UrlEncode( - \MailSo\Base\Utils::GetDomainFromEmail($sEmail), $bUrlEncode), $sLine); - $sLine = \str_replace('{user:domain-clear}', Utils::UrlEncode( - \MailSo\Base\Utils::GetClearDomainName( - \MailSo\Base\Utils::GetDomainFromEmail($sEmail)), $bUrlEncode), $sLine); + $sLine = \str_replace('{user:email}', $sEmail, $sLine); + $sLine = \str_replace('{user:login}', \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $sLine); + $sLine = \str_replace('{user:domain}', \MailSo\Base\Utils::GetDomainFromEmail($sEmail), $sLine); + $sLine = \str_replace('{user:domain-clear}', + \MailSo\Base\Utils::GetClearDomainName(\MailSo\Base\Utils::GetDomainFromEmail($sEmail)), + $sLine); } } @@ -403,6 +400,12 @@ class Actions return $this->oMailClient; } + public function ImapClient(): \MailSo\Imap\ImapClient + { +// $this->initMailClientConnection(); + return $this->MailClient()->ImapClient(); + } + // Stores data in AdditionalAccount else MainAccount public function LocalStorageProvider(): Providers\Storage { @@ -460,13 +463,13 @@ class Actions return $this->oDomainProvider; } - public function AddressBookProvider(?Model\Account $oAccount = null, bool $bForceEnable = false): Providers\AddressBook + public function AddressBookProvider(?Model\Account $oAccount = null): Providers\AddressBook { if (null === $this->oAddressBookProvider) { $oDriver = null; try { -// if ($bForceEnable || $this->oConfig->Get('contacts', 'enable', false)) { - if ($bForceEnable || $this->GetCapa(Enumerations\Capa::CONTACTS)) { +// if ($this->oConfig->Get('contacts', 'enable', false)) { + if ($this->GetCapa(Enumerations\Capa::CONTACTS)) { $oDriver = $this->fabrica('address-book', $oAccount); } if ($oAccount && $oDriver) { @@ -474,8 +477,9 @@ class Actions $oDriver->setDAVClientConfig($this->getContactsSyncData($oAccount)); } } catch (\Throwable $e) { + \SnappyMail\LOG::error('AddressBook', $e->getMessage()."\n".$e->getTraceAsString()); $oDriver = null; - \SnappyMail\LOG::error('AddressBook', $e->getMessage()); +// $oDriver = new Providers\AddressBook\PdoAddressBook(); } $this->oAddressBookProvider = new Providers\AddressBook($oDriver); $this->oAddressBookProvider->SetLogger($this->oLogger); @@ -505,7 +509,10 @@ class Actions switch (true) { default: case $bForceFile: - $oDriver = new \MailSo\Cache\Drivers\File(APP_PRIVATE_DATA . 'cache', $sKey); + $oDriver = new \MailSo\Cache\Drivers\File( + \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache', + $sKey + ); break; case ('APCU' === $sDriver) && @@ -550,11 +557,6 @@ class Actions return $this->oPlugins; } - public function Logger(): \MailSo\Log\Logger - { - return $this->oLogger; - } - public function LoggerAuth(): \MailSo\Log\Logger { if (!$this->oLoggerAuth) { @@ -562,13 +564,10 @@ class Actions if ($this->oConfig->Get('logs', 'auth_logging', false)) { // $this->oLoggerAuth->SetLevel(\LOG_WARNING); - $sAuthLogFileFullPath = \APP_PRIVATE_DATA . 'logs/' . $this->compileLogFileName( - $this->oConfig->Get('logs', 'auth_logging_filename', '')); + $sAuthLogFileFullPath = (\trim($this->oConfig->Get('logs', 'path', '') ?: \APP_PRIVATE_DATA . 'logs')) + . '/' . $this->compileLogFileName($this->oConfig->Get('logs', 'auth_logging_filename', '')); $sLogFileDir = \dirname($sAuthLogFileFullPath); - if (!\is_dir($sLogFileDir)) { - \mkdir($sLogFileDir, 0755, true); - } - + \is_dir($sLogFileDir) || \mkdir($sLogFileDir, 0755, true); $this->oLoggerAuth->append( (new \MailSo\Log\Drivers\File($sAuthLogFileFullPath)) ->DisableTimePrefix() @@ -584,13 +583,13 @@ class Actions { $sLine = $this->oConfig->Get('logs', 'auth_logging_format', ''); if (!empty($sLine)) { - $this->LoggerAuth()->Write($this->compileLogParams($sLine, $oAccount, false, $aAdditionalParams)); + $this->LoggerAuth()->Write($this->compileLogParams($sLine, $oAccount, $aAdditionalParams), \LOG_WARNING); } if (($this->oConfig->Get('logs', 'auth_logging', false) || $this->oConfig->Get('logs', 'auth_syslog', false)) && \openlog('snappymail', 0, \LOG_AUTHPRIV)) { \syslog(\LOG_ERR, $this->compileLogParams( $admin ? 'Admin Auth failed: ip={request:ip} user={user:login}' : 'Auth failed: ip={request:ip} user={imap:login}', - $oAccount, false, $aAdditionalParams + $oAccount, $aAdditionalParams )); \closelog(); } @@ -603,60 +602,61 @@ class Actions $aResult = array( 'Auth' => false, - 'Title' => $oConfig->Get('webmail', 'title', 'SnappyMail Webmail'), - 'LoadingDescription' => $oConfig->Get('webmail', 'loading_description', 'SnappyMail'), + 'title' => $oConfig->Get('webmail', 'title', 'SnappyMail Webmail'), + 'loadingDescription' => $oConfig->Get('webmail', 'loading_description', 'SnappyMail'), 'Plugins' => array(), 'System' => \array_merge( array( 'version' => APP_VERSION, - 'token' => $oConfig->Get('security', 'csrf_protection', false) ? Utils::GetCsrfToken() : '', + 'token' => Utils::GetCsrfToken(), 'languages' => \SnappyMail\L10n::getLanguages(false), 'webPath' => \RainLoop\Utils::WebPath(), 'webVersionPath' => \RainLoop\Utils::WebVersionPath() ), $bAdmin ? array( - 'adminHostUse' => '' !== $oConfig->Get('security', 'admin_panel_host', ''), + 'adminHost' => '' !== $oConfig->Get('security', 'admin_panel_host', ''), 'adminPath' => $oConfig->Get('security', 'admin_panel_key', '') ?: 'admin', 'adminAllowed' => (bool)$oConfig->Get('security', 'allow_admin_panel', true) ) : array() ), - 'AllowLanguagesOnLogin' => (bool) $oConfig->Get('login', 'allow_languages_on_login', true) + 'allowLanguagesOnLogin' => (bool) $oConfig->Get('login', 'allow_languages_on_login', true) ); $sLanguage = $oConfig->Get('webmail', 'language', 'en'); $UserLanguageRaw = $this->detectUserLanguage($bAdmin); if ($bAdmin) { +// $this->AdminAppData($aResult); $aResult['Auth'] = $this->IsAdminLoggined(false); if ($aResult['Auth']) { - $aResult['AdminLogin'] = (string)$oConfig->Get('security', 'admin_login', ''); - $aResult['AdminTOTP'] = (string)$oConfig->Get('security', 'admin_totp', ''); - $aResult['UseTokenProtection'] = (bool)$oConfig->Get('security', 'csrf_protection', true); - $aResult['EnabledPlugins'] = (bool)$oConfig->Get('plugins', 'enable', false); + $aResult['adminLogin'] = (string)$oConfig->Get('security', 'admin_login', ''); + $aResult['adminTOTP'] = (string)$oConfig->Get('security', 'admin_totp', ''); + $aResult['pluginsEnable'] = (bool)$oConfig->Get('plugins', 'enable', false); - $aResult['LoginDefaultDomain'] = $oConfig->Get('login', 'default_domain', ''); - $aResult['DetermineUserLanguage'] = (bool)$oConfig->Get('login', 'determine_user_language', true); - $aResult['DetermineUserDomain'] = (bool)$oConfig->Get('login', 'determine_user_domain', false); + $aResult['loginDefaultDomain'] = $oConfig->Get('login', 'default_domain', ''); + $aResult['determineUserLanguage'] = (bool)$oConfig->Get('login', 'determine_user_language', true); + $aResult['determineUserDomain'] = (bool)$oConfig->Get('login', 'determine_user_domain', false); - $aResult['VerifySslCertificate'] = (bool)$oConfig->Get('ssl', 'verify_certificate', false); - $aResult['AllowSelfSigned'] = (bool)$oConfig->Get('ssl', 'allow_self_signed', true); + $aResult['supportedPdoDrivers'] = \RainLoop\Pdo\Base::getAvailableDrivers(); - $aResult['supportedPdoDrivers'] = \RainLoop\Common\PdoAbstract::getAvailableDrivers(); + $aResult['contactsEnable'] = (bool)$oConfig->Get('contacts', 'enable', false); + $aResult['contactsSync'] = (bool)$oConfig->Get('contacts', 'allow_sync', false); + $aResult['contactsPdoType'] = Providers\AddressBook\PdoAddressBook::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); + $aResult['contactsPdoDsn'] = (string)$oConfig->Get('contacts', 'pdo_dsn', ''); + $aResult['contactsPdoType'] = (string)$oConfig->Get('contacts', 'type', ''); + $aResult['contactsPdoUser'] = (string)$oConfig->Get('contacts', 'pdo_user', ''); + $aResult['contactsPdoPassword'] = static::APP_DUMMY; + $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['contactsSuggestionsLimit'] = (int)$oConfig->Get('contacts', 'suggestions_limit', 20); - $aResult['ContactsEnable'] = (bool)$oConfig->Get('contacts', 'enable', false); - $aResult['ContactsSync'] = (bool)$oConfig->Get('contacts', 'allow_sync', false); - $aResult['ContactsPdoType'] = Providers\AddressBook\PdoAddressBook::validPdoType($this->oConfig->Get('contacts', 'type', 'sqlite')); - $aResult['ContactsPdoDsn'] = (string)$oConfig->Get('contacts', 'pdo_dsn', ''); - $aResult['ContactsPdoType'] = (string)$oConfig->Get('contacts', 'type', ''); - $aResult['ContactsPdoUser'] = (string)$oConfig->Get('contacts', 'pdo_user', ''); - $aResult['ContactsPdoPassword'] = (string)APP_DUMMY; + $aResult['faviconUrl'] = $oConfig->Get('webmail', 'favicon_url', ''); - $aResult['FaviconUrl'] = $oConfig->Get('webmail', 'favicon_url', ''); - - $aResult['WeakPassword'] = \is_file(APP_PRIVATE_DATA.'admin_password.txt'); + $aResult['weakPassword'] = \is_file(APP_PRIVATE_DATA.'admin_password.txt'); $aResult['System']['languagesAdmin'] = \SnappyMail\L10n::getLanguages(true); - $aResult['LanguageAdmin'] = $this->ValidateLanguage($oConfig->Get('webmail', 'language_admin', 'en'), '', true); - $aResult['UserLanguageAdmin'] = $this->ValidateLanguage($UserLanguageRaw, '', true, true); + $aResult['languageAdmin'] = $this->ValidateLanguage($oConfig->Get('webmail', 'language_admin', 'en'), '', true); + $aResult['languageUsers'] = $this->ValidateLanguage($UserLanguageRaw, '', true, true); } else { $passfile = APP_PRIVATE_DATA.'admin_password.txt'; $sPassword = $oConfig->Get('security', 'admin_password', ''); @@ -674,45 +674,35 @@ class Actions $aResult = \array_merge($aResult, [ 'Auth' => true, 'Email' => \MailSo\Base\Utils::IdnToUtf8($oAccount->Email()), - 'IncLogin' => $oAccount->IncLogin(), - 'OutLogin' => $oAccount->OutLogin(), - 'AccountHash' => $oAccount->Hash(), - 'AccountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]), - 'MainEmail' => \MailSo\Base\Utils::IdnToUtf8($this->getMainAccountFromToken()->Email()), - 'MailToEmail' => '', + 'accountHash' => $oAccount->Hash(), + 'accountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]), - 'ContactsIsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), - 'ContactsSyncIsAllowed' => (bool)$oConfig->Get('contacts', 'allow_sync', false), - 'ContactsSyncInterval' => (int)$oConfig->Get('contacts', 'sync_interval', 20), - 'ContactsSyncMode' => 0, - 'ContactsSyncUrl' => '', - 'ContactsSyncUser' => '', - 'ContactsSyncPassword' => '', + 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), 'ViewHTML' => (bool) $oConfig->Get('defaults', 'view_html', true), - 'ShowImages' => (bool) $oConfig->Get('defaults', 'show_images', false), + 'ViewImages' => $oConfig->Get('defaults', 'view_images', 'ask'), + 'ViewImagesWhitelist' => '', 'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false), + 'AllowStyles' => false, 'ListInlineAttachments' => false, + 'CollapseBlockquotes' => true, + 'MaxBlockquotesLevel' => 0, + 'simpleAttachmentsList' => false, + 'listGrouped' => false, 'MessagesPerPage' => (int) $oConfig->Get('webmail', 'messages_per_page', 25), 'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5), - 'MsgDefaultAction' => 1, + 'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1), 'SoundNotification' => true, 'NotificationSound' => 'new-mail', 'DesktopNotifications' => true, 'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW), 'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')), '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), - 'UseThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false), 'AllowDraftAutosave' => (bool) $oConfig->Get('defaults', 'allow_draft_autosave', true), - 'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false), 'ContactsAutosave' => (bool) $oConfig->Get('defaults', 'contacts_autosave', true), - 'HideUnsubscribed' => false, - 'HideDeleted' => true, - 'UnhideKolabFolders' => false, - 'UserBackgroundName' => '', - 'UserBackgroundHash' => '', - 'SieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false) + 'sieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false) ]); $aAttachmentsActions = array(); @@ -723,9 +713,6 @@ class Actions } $aResult['System'] = \array_merge( $aResult['System'], array( - 'allowHtmlEditorBitiButtons' => (bool)$oConfig->Get('labs', 'allow_html_editor_biti_buttons', false), - 'allowCtrlEnterOnCompose' => (bool)$oConfig->Get('labs', 'allow_ctrl_enter_on_compose', false), - 'useImapThread' => (bool)$oConfig->Get('imap', 'use_thread', false), 'allowAppendMessage' => (bool)$oConfig->Get('labs', 'allow_message_append', false), 'folderSpecLimit' => (int)$oConfig->Get('labs', 'folders_spec_limit', 50), 'listPermanentFiltered' => '' !== \trim($oConfig->Get('imap', 'message_list_permanent_filter', '')), @@ -734,49 +721,48 @@ class Actions ) ); - if ($aResult['ContactsIsAllowed'] && $aResult['ContactsSyncIsAllowed']) { - $mData = $this->getContactsSyncData($oAccount); - if (\is_array($mData)) { - $aResult['ContactsSyncMode'] = isset($mData['Mode']) ? $mData['Mode'] : 0; - $aResult['ContactsSyncUrl'] = isset($mData['Url']) ? \trim($mData['Url']) : ''; - $aResult['ContactsSyncUser'] = isset($mData['User']) ? \trim($mData['User']) : ''; - $aResult['ContactsSyncPassword'] = APP_DUMMY; - } + if ($aResult['contactsAllowed'] && $oConfig->Get('contacts', 'allow_sync', false)) { + $aData = $this->getContactsSyncData($oAccount) ?: [ + 'Mode' => 0, + 'Url' => '', + 'User' => '' + ]; + $aData['Password'] = empty($aData['Password']) ? '' : static::APP_DUMMY; + $aData['Interval'] = \max(20, \min(320, (int) $oConfig->Get('contacts', 'sync_interval', 20))); + unset($aData['PasswordHMAC']); + $aResult['ContactsSync'] = $aData; } - $sToken = Utils::GetCookie(self::AUTH_MAILTO_TOKEN_KEY, null); + $sToken = \SnappyMail\Cookies::get(self::AUTH_MAILTO_TOKEN_KEY); if (null !== $sToken) { - Utils::ClearCookie(self::AUTH_MAILTO_TOKEN_KEY); + \SnappyMail\Cookies::clear(self::AUTH_MAILTO_TOKEN_KEY); $mMailToData = Utils::DecodeKeyValuesQ($sToken); if (!empty($mMailToData['MailTo']) && 'MailTo' === $mMailToData['MailTo'] && !empty($mMailToData['To'])) { - $aResult['MailToEmail'] = \MailSo\Base\Utils::IdnToUtf8($mMailToData['To']); + $aResult['mailToEmail'] = \MailSo\Base\Utils::IdnToUtf8($mMailToData['To']); } } - $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); - if ($oSettingsLocal instanceof Settings) { - $aResult['SentFolder'] = (string)$oSettingsLocal->GetConf('SentFolder', ''); - $aResult['DraftsFolder'] = (string)$oSettingsLocal->GetConf('DraftFolder', ''); - $aResult['SpamFolder'] = (string)$oSettingsLocal->GetConf('SpamFolder', ''); - $aResult['TrashFolder'] = (string)$oSettingsLocal->GetConf('TrashFolder', ''); - $aResult['ArchiveFolder'] = (string)$oSettingsLocal->GetConf('ArchiveFolder', ''); - $aResult['HideUnsubscribed'] = (bool)$oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']); - $aResult['UseThreads'] = (bool)$oSettingsLocal->GetConf('UseThreads', $aResult['UseThreads']); - $aResult['ReplySameFolder'] = (bool)$oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']); - $aResult['HideDeleted'] = (bool)$oSettingsLocal->GetConf('HideDeleted', $aResult['HideDeleted']); - $aResult['UnhideKolabFolders'] = (bool)$oSettingsLocal->GetConf('UnhideKolabFolders', $aResult['UnhideKolabFolders']); - } - if ($oConfig->Get('login', 'determine_user_language', true)) { $sLanguage = $this->ValidateLanguage($UserLanguageRaw, $sLanguage, false); } + // MainAccount or AdditionalAccount + $aResult = \array_merge($aResult, $this->getAccountData($oAccount)); + + // MainAccount $oSettings = $this->SettingsProvider()->Load($oAccount); if ($oSettings instanceof Settings) { - if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) { - $sLanguage = (string) $oSettings->GetConf('Language', $sLanguage); +/* + foreach ($oSettings->toArray() as $key => $value) { + $aResult[\lcfirst($key)] = $value; } +*/ + if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) { + $sLanguage = (string) $oSettings->GetConf('language', $sLanguage); + } + $aResult['hourCycle'] = $oSettings->GetConf('hourCycle', ''); + if (!$oSettings->GetConf('MessagesPerPage')) { $oSettings->SetConf('MessagesPerPage', $oSettings->GetConf('MPP', $aResult['MessagesPerPage'])); } @@ -784,13 +770,23 @@ class Actions $aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType'])); $aResult['requestReadReceipt'] = (bool) $oSettings->GetConf('requestReadReceipt', false); $aResult['requestDsn'] = (bool) $oSettings->GetConf('requestDsn', false); + $aResult['requireTLS'] = (bool) $oSettings->GetConf('requireTLS', false); $aResult['pgpSign'] = (bool) $oSettings->GetConf('pgpSign', false); $aResult['pgpEncrypt'] = (bool) $oSettings->GetConf('pgpEncrypt', false); + $aResult['allowSpellcheck'] = (bool) $oSettings->GetConf('allowSpellcheck', false); +// $aResult['allowCtrlEnterOnCompose'] = (bool) $oSettings->GetConf('allowCtrlEnterOnCompose', true); $aResult['ViewHTML'] = (bool)$oSettings->GetConf('ViewHTML', $aResult['ViewHTML']); - $aResult['ShowImages'] = (bool)$oSettings->GetConf('ShowImages', $aResult['ShowImages']); + $show_images = (bool) $oSettings->GetConf('ShowImages', false); + $aResult['ViewImages'] = $oSettings->GetConf('ViewImages', $show_images ? 'always' : $aResult['ViewImages']); + $aResult['ViewImagesWhitelist'] = $oSettings->GetConf('ViewImagesWhitelist', ''); $aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']); + $aResult['AllowStyles'] = (bool)$oSettings->GetConf('AllowStyles', $aResult['AllowStyles']); $aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']); + $aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']); + $aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']); + $aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']); + $aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']); $aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']); $aResult['MessagesPerPage'] = (int)$oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']); $aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']); @@ -799,6 +795,7 @@ class Actions $aResult['NotificationSound'] = (string)$oSettings->GetConf('NotificationSound', $aResult['NotificationSound']); $aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']); $aResult['UseCheckboxesInList'] = (bool)$oSettings->GetConf('UseCheckboxesInList', $aResult['UseCheckboxesInList']); + $aResult['showNextMessage'] = (bool)$oSettings->GetConf('showNextMessage', $aResult['showNextMessage']); $aResult['AllowDraftAutosave'] = (bool)$oSettings->GetConf('AllowDraftAutosave', $aResult['AllowDraftAutosave']); $aResult['AutoLogout'] = (int)$oSettings->GetConf('AutoLogout', $aResult['AutoLogout']); $aResult['Layout'] = (int)$oSettings->GetConf('Layout', $aResult['Layout']); @@ -811,14 +808,14 @@ class Actions $aResult['fontMono'] = $oSettings->GetConf('fontMono', ''); if ($this->GetCapa(Enumerations\Capa::USER_BACKGROUND)) { - $aResult['UserBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', $aResult['UserBackgroundName']); - $aResult['UserBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', $aResult['UserBackgroundHash']); + $aResult['userBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', ''); + $aResult['userBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', ''); } } - $aResult['NewMailSounds'] = []; + $aResult['newMailSounds'] = []; foreach (\glob(APP_VERSION_ROOT_PATH.'static/sounds/*.mp3') as $file) { - $aResult['NewMailSounds'][] = \basename($file, '.mp3'); + $aResult['newMailSounds'][] = \basename($file, '.mp3'); } } else { @@ -826,7 +823,7 @@ class Actions $sLanguage = $this->ValidateLanguage($UserLanguageRaw, $sLanguage, false); } - if ('0.0.0' === APP_VERSION) { + if (SNAPPYMAIL_DEV) { $aResult['DevEmail'] = $oConfig->Get('labs', 'dev_email', ''); $aResult['DevPassword'] = $oConfig->Get('labs', 'dev_password', ''); } else { @@ -834,15 +831,13 @@ class Actions $aResult['DevPassword'] = ''; } - $aResult['SignMe'] = (string) $oConfig->Get('login', 'sign_me_auto', Enumerations\SignMeType::DEFAULT_OFF); - - $aResult['AdditionalLoginError'] = $this->GetSpecLogoutCustomMgsWithDeletion(); + $aResult['signMe'] = (string) $oConfig->Get('login', 'sign_me_auto', Enumerations\SignMeType::DEFAULT_OFF); } } if ($aResult['Auth']) { - $aResult['UseLocalProxyForExternalImages'] = (bool)$oConfig->Get('labs', 'use_local_proxy_for_external_images', false); - $aResult['AllowLanguagesOnSettings'] = (bool) $oConfig->Get('webmail', 'allow_languages_on_settings', true); + $aResult['useLocalProxyForExternalImages'] = (bool)$oConfig->Get('labs', 'use_local_proxy_for_external_images', false); + $aResult['allowLanguagesOnSettings'] = (bool) $oConfig->Get('webmail', 'allow_languages_on_settings', true); $aResult['Capa'] = $this->Capa($bAdmin, $oAccount); $value = \ini_get('upload_max_filesize'); $upload_max_filesize = \intval($value); @@ -851,26 +846,25 @@ class Actions case 'M': $upload_max_filesize *= 1024; case 'K': $upload_max_filesize *= 1024; } - $aResult['AttachmentLimit'] = \min($upload_max_filesize, ((int) $oConfig->Get('webmail', 'attachment_size_limit', 10)) * 1024 * 1024); - $aResult['PhpUploadSizes'] = array( + $aResult['attachmentLimit'] = \min($upload_max_filesize, ((int) $oConfig->Get('webmail', 'attachment_size_limit', 10)) * 1024 * 1024); + $aResult['phpUploadSizes'] = array( 'upload_max_filesize' => $value, 'post_max_size' => \ini_get('post_max_size') ); $aResult['System']['themes'] = $this->GetThemes(); } - $sStaticCache = $this->StaticCache(); - $aResult['Theme'] = $this->GetTheme($bAdmin); - $aResult['Language'] = $this->ValidateLanguage($sLanguage, '', false); - $aResult['UserLanguage'] = $this->ValidateLanguage($UserLanguageRaw, '', false, true); + $aResult['language'] = $this->ValidateLanguage($sLanguage, '', false); + $aResult['userLanguage'] = $this->ValidateLanguage($UserLanguageRaw, '', false, true); $aResult['PluginsLink'] = $this->oPlugins->HaveJs($bAdmin) - ? './?/Plugins/0/' . ($bAdmin ? 'Admin' : 'User') . '/' . $sStaticCache . '/' + ? 'Plugins/0/' . ($bAdmin ? 'Admin' : 'User') . '/' . $this->etag($this->oPlugins->Hash()) . '/' : ''; - $bAppJsDebug = $this->oConfig->Get('labs', 'use_app_debug_js', false); + $bAppJsDebug = $this->oConfig->Get('debug', 'javascript', false) + || $this->oConfig->Get('debug', 'enable', false); $aResult['StaticLibsJs'] = Utils::WebStaticPath('js/' . ($bAppJsDebug ? '' : 'min/') . 'libs' . ($bAppJsDebug ? '' : '.min') . '.js'); @@ -880,25 +874,20 @@ class Actions return $aResult; } - protected function requestSleep(int $iDelay = 1): void - { - $time = \microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']; - if ($iDelay > $time) { - \usleep(($iDelay - $time) * 1000000); - } - } - protected function loginErrorDelay(): void { - $iDelay = (int)$this->oConfig->Get('labs', 'login_fault_delay', 0); + $iDelay = (int) $this->oConfig->Get('labs', 'login_fault_delay', 0); if (0 < $iDelay) { - $this->requestSleep($iDelay); + $seconds = $iDelay - (\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']); + if (0 < $seconds) { + \usleep(\intval($seconds * 1000000)); + } } } protected function getAdditionalLogParamsByUserLogin(string $sLogin, bool $bAdmin = false): array { - $sHost = $bAdmin ? $this->Http()->GetHost(false, true, true) : \MailSo\Base\Utils::GetDomainFromEmail($sLogin); + $sHost = $bAdmin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::GetDomainFromEmail($sLogin); return array( '{imap:login}' => $sLogin, '{imap:host}' => $sHost, @@ -912,13 +901,12 @@ class Actions public function DoPing(): array { - return $this->DefaultResponse(__FUNCTION__, 'Pong'); + return $this->DefaultResponse('Pong'); } public function DoVersion(): array { - return $this->DefaultResponse(__FUNCTION__, - APP_VERSION === (string)$this->GetActionParam('Version', '')); + return $this->DefaultResponse(APP_VERSION === (string)$this->GetActionParam('version', '')); } public function MainClearFileName(string $sFileName, string $sContentType, string $sMimeIndex, int $iMaxLength = 250): string @@ -934,82 +922,38 @@ class Actions return \MailSo\Base\Utils::SecureFileName(\MailSo\Base\Utils::Utf8Clear($sClearedFileName)); } - protected function getUploadErrorMessageByCode(int $iError, int &$iClientError): string - { - $sError = ''; - $iClientError = UploadError::NORMAL; - switch ($iError) { - case UPLOAD_ERR_OK: - break; - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - case UploadError::CONFIG_SIZE: - case UploadError::EMPTY_FILES_DATA: - $sError = 'File is too big'; - $iClientError = UploadError::FILE_IS_TOO_BIG; - break; - case UPLOAD_ERR_PARTIAL: - $sError = 'File partially uploaded'; - $iClientError = UploadError::FILE_PARTIALLY_UPLOADED; - break; - case UPLOAD_ERR_NO_FILE: - $sError = 'No file uploaded'; - $iClientError = UploadError::FILE_NO_UPLOADED; - break; - case UPLOAD_ERR_NO_TMP_DIR: - case UPLOAD_ERR_CANT_WRITE: - case UPLOAD_ERR_EXTENSION: - $sError = 'Missing temp folder'; - $iClientError = UploadError::MISSING_TEMP_FOLDER; - break; - case UploadError::ON_SAVING: - $sError = 'Error on saving file'; - $iClientError = UploadError::FILE_ON_SAVING_ERROR; - break; - case UploadError::FILE_TYPE: - $sError = 'Invalid file type'; - $iClientError = UploadError::FILE_TYPE; - break; - case UploadError::UNKNOWN: - default: - $sError = 'Unknown error'; - $iClientError = UploadError::UNKNOWN; - break; - } - - return $sError; - } - - public function Upload(): array + public function Upload(?array $aFile, int $iError): array { $oAccount = $this->getAccountFromToken(); $aResponse = array(); - $aFile = $this->GetActionParam('File', null); - $iError = $this->GetActionParam('Error', Enumerations\UploadError::UNKNOWN); - if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { $sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']); + + // Detect content-type + $type = \SnappyMail\File\MimeType::fromFile($aFile['tmp_name'], $aFile['name']) + ?: \SnappyMail\File\MimeType::fromFilename($aFile['name']); + if ($type) { + $aFile['type'] = $type; + $sSavedName .= \SnappyMail\File\MimeType::toExtension($type); + } + if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { $iError = Enumerations\UploadError::ON_SAVING; } else { - $sUploadName = $aFile['name']; - $iSize = $aFile['size']; - $sMimeType = $aFile['type']; - $aResponse['Attachment'] = array( - 'Name' => $sUploadName, - 'TempName' => $sSavedName, - 'MimeType' => $sMimeType, - 'Size' => (int)$iSize + 'name' => $aFile['name'], + 'tempName' => $sSavedName, + 'mimeType' => $aFile['type'], + 'size' => (int) $aFile['size'] ); } } if (UPLOAD_ERR_OK !== $iError) { - $iClientError = Enumerations\UploadError::NORMAL; - $sError = $this->getUploadErrorMessageByCode($iError, $iClientError); + $iClientError = 0; + $sError = Enumerations\UploadError::getUserMessage($iError, $iClientError); if (!empty($sError)) { $aResponse['ErrorCode'] = $iClientError; @@ -1017,88 +961,7 @@ class Actions } } - return $this->DefaultResponse(__FUNCTION__, $aResponse); - } - - public function UploadBackground(): array - { - $oAccount = $this->getAccountFromToken(); - - if (!$this->GetCapa(Enumerations\Capa::USER_BACKGROUND)) { - return $this->FalseResponse(__FUNCTION__); - } - - $sName = ''; - $sHash = ''; - - $aFile = $this->GetActionParam('File', null); - $iError = $this->GetActionParam('Error', Enumerations\UploadError::UNKNOWN); - - if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { - $sMimeType = \strtolower(\MailSo\Base\Utils::MimeContentType($aFile['name'])); - if (\in_array($sMimeType, array('image/png', 'image/jpg', 'image/jpeg'))) { - $sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']); - if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { - $iError = Enumerations\UploadError::ON_SAVING; - } else { - $rData = $this->FilesProvider()->GetFile($oAccount, $sSavedName); - if (\is_resource($rData)) { - $sData = \stream_get_contents($rData); - if (!empty($sData) && \strlen($sData)) { - $sName = $aFile['name']; - if (empty($sName)) { - $sName = '_'; - } - - if ($this->StorageProvider()->Put($oAccount, - Providers\Storage\Enumerations\StorageType::CONFIG, - 'background', - Utils::jsonEncode(array( - 'Name' => $aFile['name'], - 'ContentType' => $sMimeType, - 'Raw' => \base64_encode($sData) - )) - )) { - $oSettings = $this->SettingsProvider()->Load($oAccount); - if ($oSettings) { - $sHash = \MailSo\Base\Utils::Sha1Rand($sName . APP_VERSION . APP_SALT); - - $oSettings->SetConf('UserBackgroundName', $sName); - $oSettings->SetConf('UserBackgroundHash', $sHash); - $this->SettingsProvider()->Save($oAccount, $oSettings); - } - } - } - - unset($sData); - } - - if (\is_resource($rData)) { - \fclose($rData); - } - - unset($rData); - } - - $this->FilesProvider()->Clear($oAccount, $sSavedName); - } else { - $iError = Enumerations\UploadError::FILE_TYPE; - } - } - - if (UPLOAD_ERR_OK !== $iError) { - $iClientError = Enumerations\UploadError::NORMAL; - $sError = $this->getUploadErrorMessageByCode($iError, $iClientError); - - if (!empty($sError)) { - return $this->FalseResponse(__FUNCTION__, $iClientError, $sError); - } - } - - return $this->DefaultResponse(__FUNCTION__, !empty($sName) && !empty($sHash) ? array( - 'Name' => $sName, - 'Hash' => $sHash - ) : false); + return $this->DefaultResponse($aResponse); } public function Capa(bool $bAdmin, ?Model\Account $oAccount = null): array @@ -1108,14 +971,18 @@ class Actions $oConfig = $this->oConfig; $aResult = array( 'AdditionalAccounts' => (bool) $oConfig->Get('webmail', 'allow_additional_accounts', false), - 'AttachmentThumbnails' => (bool) $oConfig->Get('interface', 'show_attachment_thumbnail', true), + 'AttachmentThumbnails' => (bool) $oConfig->Get('interface', 'show_attachment_thumbnail', true) + && ($bAdmin + || \extension_loaded('gd') + || \extension_loaded('gmagick') + || \extension_loaded('imagick') + ), 'AttachmentsActions' => (bool) $oConfig->Get('capa', 'attachments_actions', false), 'Contacts' => (bool) $oConfig->Get('contacts', 'enable', false), 'DangerousActions' => (bool) $oConfig->Get('capa', 'dangerous_actions', true), 'GnuPG' => (bool) $oConfig->Get('security', 'openpgp', false) && \SnappyMail\PGP\GnuPG::isSupported(), 'Identities' => (bool) $oConfig->Get('webmail', 'allow_additional_identities', false), 'Kolab' => false, // See Kolab plugin - 'MessageActions' => (bool) $oConfig->Get('capa', 'message_actions', true), 'OpenPGP' => (bool) $oConfig->Get('security', 'openpgp', false), 'Quota' => (bool) $oConfig->Get('capa', 'quota', true), 'Sieve' => false, @@ -1134,36 +1001,29 @@ class Actions public function etag(string $sKey): string { - return \md5('Etag:' . \md5($sKey . \md5($this->oConfig->Get('cache', 'index', '')))); +// if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) { + return \md5($sKey . $this->oConfig->Get('cache', 'index', '') . APP_VERSION); } - public function cacheByKey(string $sKey, bool $bForce = false): bool + public function cacheByKey(string $sKey): bool { - $bResult = false; - if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { - $iExpires = $this->oConfig->Get('cache', 'http_expires', 3600); - if (0 < $iExpires) { - $this->Http()->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires); - $bResult = true; - } + if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) { + \MailSo\Base\Http::ServerUseCache( + $this->etag($sKey), + 0, // issue with messages + $this->oConfig->Get('cache', 'http_expires', 3600) + ); + return true; } - - if (!$bResult) { - $this->Http()->ServerNoCache(); - } - - return $bResult; + $this->Http()->ServerNoCache(); + return false; } - public function verifyCacheByKey(string $sKey, bool $bForce = false): void + public function verifyCacheByKey(string $sKey): void { - if (!empty($sKey) && ($bForce || $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true))) { - $sIfNoneMatch = $this->Http()->GetHeader('If-None-Match', ''); - if ($this->etag($sKey) === $sIfNoneMatch) { - \MailSo\Base\Http::StatusHeader(304); - $this->cacheByKey($sKey); - exit(0); - } + if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) { + \MailSo\Base\Http::checkETag($this->etag($sKey)); +// \MailSo\Base\Http::checkLastModified(0); } } @@ -1174,9 +1034,9 @@ class Actions { $oAccount = $this->getAccountFromToken(); - if (!$this->MailClient()->IsLoggined()) { + if ($oAccount && !$this->ImapClient()->IsLoggined()) { try { - $oAccount->ImapConnectAndLoginHelper($this->oPlugins, $this->MailClient(), $this->oConfig); + $oAccount->ImapConnectAndLogin($this->oPlugins, $this->ImapClient(), $this->oConfig); } catch (\MailSo\Net\Exceptions\ConnectionException $oException) { throw new Exceptions\ClientException(Notifications::ConnectionError, $oException); } catch (\Throwable $oException) { @@ -1187,18 +1047,21 @@ class Actions return $oAccount; } - protected function getDecodedRawKeyValue(string $sRawKey): array + public function encodeRawKey(array $aValues): string { - return empty($sRawKey) ? array() : Utils::DecodeKeyValuesQ($sRawKey); + $aValues['accountHash'] = $this->getAccountFromToken()->Hash(); + return \MailSo\Base\Utils::UrlSafeBase64Encode(\json_encode($aValues)); } - public function StaticCache(): string + public function decodeRawKey(string $sRawKey): array { - static $sCache = null; - if (!$sCache) { - $sCache = \md5(APP_VERSION . $this->oPlugins->Hash()); + return empty($sRawKey) ? [] + : (\json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($sRawKey), true) ?: []); +/* + if (empty($aValues['accountHash']) || $aValues['accountHash'] !== $oAccount->Hash()) { + return []; } - return $sCache; +*/ } public function SetActionParams(array $aCurrentActionParams, string $sMethodName = ''): self @@ -1231,10 +1094,10 @@ class Actions return isset($this->aCurrentActionParams[$sKey]); } - public function Location(string $sUrl): void + public function Location(string $sUrl, int $iStatus = 302): void { - $this->oLogger->Write('Location: ' . $sUrl); - \header('Location: ' . $sUrl); + $this->logWrite("{$iStatus} Location: {$sUrl}"); + \MailSo\Base\Http::Location($sUrl, $iStatus); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php index 84c20a709..7316ddc2e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -15,10 +15,7 @@ use RainLoop\Utils; trait Accounts { - /** - * @var RainLoop\Providers\Identities - */ - private $oIdentitiesProvider; + private ?\RainLoop\Providers\Identities $oIdentitiesProvider = null; protected function GetMainEmail(Account $oAccount) { @@ -41,7 +38,8 @@ trait Accounts StorageType::CONFIG, 'additionalaccounts' ); - $aAccounts = $sAccounts ? \json_decode($sAccounts, true) : \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount); + $aAccounts = $sAccounts ? \json_decode($sAccounts, true) + : \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount); if ($aAccounts && \is_array($aAccounts)) { return $aAccounts; } @@ -77,14 +75,15 @@ trait Accounts $oMainAccount = $this->getMainAccountFromToken(); if (!$this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } $aAccounts = $this->GetAccounts($oMainAccount); - $sEmail = \trim($this->GetActionParam('Email', '')); - $sPassword = $this->GetActionParam('Password', ''); - $bNew = '1' === (string)$this->GetActionParam('New', '1'); + $sEmail = \trim($this->GetActionParam('email', '')); + $sPassword = $this->GetActionParam('password', ''); + $sName = \trim($this->GetActionParam('name', '')); + $bNew = !empty($this->GetActionParam('new', 1)); $sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); if ($bNew && ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail]))) { @@ -93,12 +92,73 @@ trait Accounts throw new ClientException(Notifications::AccountDoesNotExist); } - $oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, false); + if ($bNew || $sPassword) { + $oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, false); + $aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount); + } else { + $aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]); + } - $aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount); - $this->SetAccounts($oMainAccount, $aAccounts); + if ($aAccounts[$sEmail]) { + $aAccounts[$sEmail]['name'] = $sName; + $this->SetAccounts($oMainAccount, $aAccounts); + } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); + } + + protected function loadAdditionalAccountImapClient(string $sEmail): \MailSo\Imap\ImapClient + { + $sEmail = \MailSo\Base\Utils::IdnToAscii(\trim($sEmail), true); + if (!\strlen($sEmail)) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + + $oMainAccount = $this->getMainAccountFromToken(); + $aAccounts = $this->GetAccounts($oMainAccount); + if (!isset($aAccounts[$sEmail])) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + $oAccount = AdditionalAccount::NewInstanceFromTokenArray($this, $aAccounts[$sEmail]); + if (!$oAccount) { + throw new ClientException(Notifications::AccountDoesNotExist); + } + + $oImapClient = new \MailSo\Imap\ImapClient; + $oImapClient->SetLogger($this->Logger()); + $this->imapConnect($oAccount, false, $oImapClient); + return $oImapClient; + } + + public function DoAccountUnread(): array + { + $oImapClient = $this->loadAdditionalAccountImapClient($this->GetActionParam('email', '')); + $oInfo = $oImapClient->FolderStatus('INBOX'); + return $this->DefaultResponse([ + 'unreadEmails' => \max(0, $oInfo->UNSEEN) + ]); + } + + /** + * Imports all mail from AdditionalAccount into MainAccount + */ + public function DoAccountImport(): array + { + $sEmail = $this->GetActionParam('email', ''); + $oImapSource = $this->loadAdditionalAccountImapClient($sEmail); + + $oMainAccount = $this->getMainAccountFromToken(); + $oImapTarget = new \MailSo\Imap\ImapClient; + $oImapTarget->SetLogger($this->Logger()); + $this->imapConnect($oMainAccount, false, $oImapTarget); + + $oSync = new \SnappyMail\Imap\Sync; + $oSync->oImapSource = $oImapSource; + $oSync->oImapTarget = $oImapTarget; + + $rootfolder = $this->GetActionParam('rootfolder', '') ?: $sEmail; + $oSync->import($rootfolder); + exit; } /** @@ -109,10 +169,10 @@ trait Accounts $oMainAccount = $this->getMainAccountFromToken(); if (!$this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } - $sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', '')); + $sEmailToDelete = \trim($this->GetActionParam('emailToDelete', '')); $sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true); $aAccounts = $this->GetAccounts($oMainAccount); @@ -121,17 +181,59 @@ trait Accounts $bReload = false; $oAccount = $this->getAccountFromToken(); if ($oAccount instanceof AdditionalAccount && $oAccount->Email() === $sEmailToDelete) { - Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); + \SnappyMail\Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); $bReload = true; } unset($aAccounts[$sEmailToDelete]); $this->SetAccounts($oMainAccount, $aAccounts); - return $this->TrueResponse(__FUNCTION__, array('Reload' => $bReload)); + return $this->TrueResponse(array('Reload' => $bReload)); } - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); + } + + public function getAccountData(Account $oAccount): array + { + $oConfig = $this->Config(); + $aResult = [ + 'Email' => $oAccount->Email(), + 'accountHash' => $oAccount->Hash(), + 'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(), + 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), + 'HideUnsubscribed' => false, + 'UseThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false), + 'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false), + 'HideDeleted' => true, + 'ShowUnreadCount' => false, + 'UnhideKolabFolders' => false, + 'CheckMailInterval' => 15 + ]; + $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + if ($oSettingsLocal instanceof \RainLoop\Settings) { + $aResult['SentFolder'] = (string) $oSettingsLocal->GetConf('SentFolder', ''); + $aResult['DraftsFolder'] = (string) $oSettingsLocal->GetConf('DraftsFolder', ''); + $aResult['JunkFolder'] = (string) $oSettingsLocal->GetConf('JunkFolder', ''); + $aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', ''); + $aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', ''); + $aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']); + $aResult['UseThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['UseThreads']); + $aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']); + $aResult['HideDeleted'] = (bool)$oSettingsLocal->GetConf('HideDeleted', $aResult['HideDeleted']); + $aResult['ShowUnreadCount'] = (bool)$oSettingsLocal->GetConf('ShowUnreadCount', $aResult['ShowUnreadCount']); + $aResult['UnhideKolabFolders'] = (bool)$oSettingsLocal->GetConf('UnhideKolabFolders', $aResult['UnhideKolabFolders']); + $aResult['CheckMailInterval'] = (int)$oSettingsLocal->GetConf('CheckMailInterval', $aResult['CheckMailInterval']); +/* + foreach ($oSettingsLocal->toArray() as $key => $value) { + $aResult[\lcfirst($key)] = $value; + } + $aResult['junkFolder'] = $aResult['spamFolder']; + unset($aResult['checkableFolder']); + unset($aResult['theme']); +*/ + } + return $aResult; } /** @@ -141,32 +243,11 @@ trait Accounts { if ($this->switchAccount(\trim($this->GetActionParam('Email', '')))) { $oAccount = $this->getAccountFromToken(); - $aResult['Email'] = $oAccount->Email(); - $aResult['IncLogin'] = $oAccount->IncLogin(); - $aResult['OutLogin'] = $oAccount->OutLogin(); - $aResult['AccountHash'] = $oAccount->Hash(); - $aResult['MainEmail'] = ($oAccount instanceof AdditionalAccount) - ? $oAccount->ParentEmail() : ''; - $aResult['ContactsIsAllowed'] = $this->AddressBookProvider($oAccount)->IsActive(); - $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); - if ($oSettingsLocal instanceof \RainLoop\Settings) { - $oConfig = $this->Config(); - $aResult['SentFolder'] = (string) $oSettingsLocal->GetConf('SentFolder', ''); - $aResult['DraftsFolder'] = (string) $oSettingsLocal->GetConf('DraftFolder', ''); - $aResult['SpamFolder'] = (string) $oSettingsLocal->GetConf('SpamFolder', ''); - $aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', ''); - $aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', ''); - $aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', false); - $aResult['UseThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $oConfig->Get('defaults', 'mail_use_threads', false)); - $aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $oConfig->Get('defaults', 'mail_reply_same_folder', false)); - $aResult['HideDeleted'] = (bool) $oSettingsLocal->GetConf('HideDeleted', true); - $aResult['UnhideKolabFolders'] = (bool) $oSettingsLocal->GetConf('UnhideKolabFolders', false); - } + $aResult = $this->getAccountData($oAccount); // $this->Plugins()->InitAppData($bAdmin, $aResult, $oAccount); - - return $this->DefaultResponse(__FUNCTION__, $aResult); + return $this->DefaultResponse($aResult); } - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } /** @@ -182,7 +263,7 @@ trait Accounts } $this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity); - return $this->DefaultResponse(__FUNCTION__, true); + return $this->TrueResponse(); } /** @@ -193,16 +274,16 @@ trait Accounts $oAccount = $this->getAccountFromToken(); if (!$this->GetCapa(Capa::IDENTITIES)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } - $sId = \trim($this->GetActionParam('IdToDelete', '')); + $sId = \trim($this->GetActionParam('idToDelete', '')); if (empty($sId)) { throw new ClientException(Notifications::UnknownError); } $this->IdentitiesProvider()->DeleteIdentity($oAccount, $sId); - return $this->DefaultResponse(__FUNCTION__, true); + return $this->TrueResponse(); } /** @@ -214,7 +295,7 @@ trait Accounts $aIdentities = $this->GetActionParam('Identities', null); if (!\is_array($aAccounts) && !\is_array($aIdentities)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } if (\is_array($aAccounts) && 1 < \count($aAccounts)) { @@ -226,7 +307,7 @@ trait Accounts $this->SetAccounts($oAccount, $aAccounts); } - return $this->DefaultResponse(__FUNCTION__, $this->LocalStorageProvider()->Put( + return $this->DefaultResponse($this->LocalStorageProvider()->Put( $this->getAccountFromToken(), StorageType::CONFIG, 'identities_order', @@ -241,11 +322,16 @@ trait Accounts */ public function DoAccountsAndIdentities(): array { - return $this->DefaultResponse(__FUNCTION__, array( - 'Accounts' => \array_map( - 'MailSo\\Base\\Utils::IdnToUtf8', - \array_keys($this->GetAccounts($this->getMainAccountFromToken())) - ), + // https://github.com/the-djmaze/snappymail/issues/571 + return $this->DefaultResponse(array( + 'Accounts' => \array_values(\array_map(function($value){ + return [ + 'email' => \MailSo\Base\Utils::IdnToUtf8($value['email'] ?? $value[1]), + 'name' => $value['name'] ?? '' + ]; + }, + $this->GetAccounts($this->getMainAccountFromToken()) + )), 'Identities' => $this->GetIdentities($this->getAccountFromToken()) )); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php index e6b17a63a..7e31d78b1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php @@ -7,11 +7,9 @@ use RainLoop\KeyPathHelper; use RainLoop\Notifications; use RainLoop\Utils; -//define('APP_DEV_VERSION', '0.0.0'); - trait Admin { - protected static $AUTH_ADMIN_TOKEN_KEY = 'smadmin'; + protected static string $AUTH_ADMIN_TOKEN_KEY = 'smadmin'; public function IsAdminLoggined(bool $bThrowExceptionOnFalse = true) : bool { @@ -22,8 +20,7 @@ trait Admin } } - if ($bThrowExceptionOnFalse) - { + if ($bThrowExceptionOnFalse) { throw new ClientException(Notifications::AuthError); } @@ -32,13 +29,13 @@ trait Admin protected function getAdminAuthKey() : string { - $cookie = Utils::GetCookie(static::$AUTH_ADMIN_TOKEN_KEY, ''); + $cookie = \SnappyMail\Cookies::get(static::$AUTH_ADMIN_TOKEN_KEY); if ($cookie) { $aAdminHash = Utils::DecodeKeyValuesQ($cookie); if (!empty($aAdminHash[1]) && 'token' === $aAdminHash[0]) { return $aAdminHash[1]; } - Utils::ClearCookie(static::$AUTH_ADMIN_TOKEN_KEY); + \SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY); } return ''; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminDomains.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminDomains.php new file mode 100644 index 000000000..33dc8406a --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminDomains.php @@ -0,0 +1,205 @@ +IsAdminLoggined(); + + return $this->DefaultResponse($this->DomainProvider()->Load($this->GetActionParam('name', ''), false, false)); + } + + public function DoAdminDomainList() : array + { + $this->IsAdminLoggined(); + $bIncludeAliases = !empty($this->GetActionParam('includeAliases', '1')); + return $this->DefaultResponse($this->DomainProvider()->GetList($bIncludeAliases)); + } + + public function DoAdminDomainDelete() : array + { + $this->IsAdminLoggined(); + + return $this->DefaultResponse($this->DomainProvider()->Delete((string) $this->GetActionParam('name', ''))); + } + + public function DoAdminDomainDisable() : array + { + $this->IsAdminLoggined(); + + return $this->DefaultResponse($this->DomainProvider()->Disable( + (string) $this->GetActionParam('name', ''), + !empty($this->GetActionParam('disabled', '0')) + )); + } + + public function DoAdminDomainSave() : array + { + $this->IsAdminLoggined(); + + $oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this); + + return $this->DefaultResponse($oDomain ? $this->DomainProvider()->Save($oDomain) : false); + } + + public function DoAdminDomainAliasSave() : array + { + $this->IsAdminLoggined(); + + return $this->DefaultResponse($this->DomainProvider()->SaveAlias( + (string) $this->GetActionParam('name', ''), + (string) $this->GetActionParam('alias', '') + )); + } + + public function DoAdminDomainMatch() : array + { + $sEmail = $this->GetActionParam('username'); + $sPassword = '********'; + $sLogin = ''; + $this->resolveLoginCredentials($sEmail, $sPassword, $sLogin); + $oDomain = \str_contains($sEmail, '@') + ? $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true) + : null; + return $this->DefaultResponse(array( + 'email' => $sEmail, + 'login' => $sLogin, + 'domain' => $oDomain, + 'whitelist' => $oDomain ? $oDomain->ValidateWhiteList($sEmail, $sLogin) : null + )); + } + + public function DoAdminDomainTest() : array + { + $this->IsAdminLoggined(); + + $bImapResult = false; + $sImapErrorDesc = ''; + $bSmtpResult = false; + $sSmtpErrorDesc = ''; + $bSieveResult = false; + $sSieveErrorDesc = ''; + + $oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this, 'test.example.com'); + if ($oDomain) { + $aAuth = $this->GetActionParam('auth'); + + try + { + $oImapClient = new \MailSo\Imap\ImapClient(); + $oImapClient->SetLogger($this->Logger()); + + $oSettings = $oDomain->ImapSettings(); + $oImapClient->Connect($oSettings); + + if (!empty($aAuth['user'])) { + $oSettings->Login = $aAuth['user']; + $oSettings->Password = $aAuth['pass']; + $oImapClient->Login($oSettings); + } + + $oImapClient->Disconnect(); + $bImapResult = true; + } + catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) + { + $this->logException($oException, \LOG_ERR); + $sImapErrorDesc = $oException->getSocketMessage(); + if (empty($sImapErrorDesc)) { + $sImapErrorDesc = $oException->getMessage(); + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + $sImapErrorDesc = $oException->getMessage(); + } + + if ($oDomain->OutUsePhpMail()) { + $bSmtpResult = \MailSo\Base\Utils::FunctionCallable('mail'); + if (!$bSmtpResult) { + $sSmtpErrorDesc = 'PHP: mail() function is undefined'; + } + } else { + try + { + $oSmtpClient = new \MailSo\Smtp\SmtpClient(); + $oSmtpClient->SetLogger($this->Logger()); + + $oSettings = $oDomain->SmtpSettings(); + $oSettings->Ehlo = \MailSo\Smtp\SmtpClient::EhloHelper(); + $oSmtpClient->Connect($oSettings); + + if (!empty($aAuth['user'])) { + $oSettings->Login = $aAuth['user']; + $oSettings->Password = $aAuth['pass']; + $oSmtpClient->Login($oSettings); + } + + $oSmtpClient->Disconnect(); + $bSmtpResult = true; + } + catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) + { + $this->logException($oException, \LOG_ERR); + $sSmtpErrorDesc = $oException->getSocketMessage(); + if (empty($sSmtpErrorDesc)) { + $sSmtpErrorDesc = $oException->getMessage(); + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + $sSmtpErrorDesc = $oException->getMessage(); + } + } + + if ($oDomain->UseSieve()) { + try + { + $oSieveClient = new \MailSo\Sieve\SieveClient(); + $oSieveClient->SetLogger($this->Logger()); + + $oSettings = $oDomain->SieveSettings(); + $oSieveClient->Connect($oSettings); + + if (!empty($aAuth['user'])) { + $oSettings->Login = $aAuth['user']; + $oSettings->Password = $aAuth['pass']; + $oSieveClient->Login($oSettings); + } + + $oSieveClient->Disconnect(); + $bSieveResult = true; + } + catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) + { + $this->logException($oException, \LOG_ERR); + $sSieveErrorDesc = $oException->getSocketMessage(); + if (empty($sSieveErrorDesc)) { + $sSieveErrorDesc = $oException->getMessage(); + } + } + catch (\Throwable $oException) + { + $this->logException($oException, \LOG_ERR); + $sSieveErrorDesc = $oException->getMessage(); + } + } else { + $bSieveResult = true; + } + } + + return $this->DefaultResponse(array( + 'Imap' => $bImapResult ? true : $sImapErrorDesc, + 'Smtp' => $bSmtpResult ? true : $sSmtpErrorDesc, + 'Sieve' => $bSieveResult ? true : $sSieveErrorDesc + )); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminExtensions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminExtensions.php new file mode 100644 index 000000000..c8070b0ac --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/AdminExtensions.php @@ -0,0 +1,145 @@ +DefaultResponse(Repository::getPackagesList()); + } + + public function DoAdminPackageDelete() : array + { + return $this->DefaultResponse(Repository::deletePackage($this->GetActionParam('id', ''))); + } + + public function DoAdminPackageInstall() : array + { + $sType = $this->GetActionParam('type', ''); + $bResult = Repository::installPackage( + $sType, + $this->GetActionParam('id', ''), + $this->GetActionParam('file', '') + ); + return $this->DefaultResponse($bResult ? + ('plugin' !== $sType ? array('Reload' => true) : true) : false); + } + + public function DoAdminPluginDisable() : array + { + $this->IsAdminLoggined(); + + $sId = (string) $this->GetActionParam('id', ''); + $bDisable = '1' === (string) $this->GetActionParam('disabled', '1'); + + if (!$bDisable) { + $oPlugin = $this->Plugins()->CreatePluginByName($sId); + if ($oPlugin) { + $sValue = $oPlugin->Supported(); + if (\strlen($sValue)) { + return $this->FalseResponse(Notifications::UnsupportedPluginPackage, $sValue); + } + } else { + return $this->FalseResponse(Notifications::InvalidPluginPackage); + } + } + + return $this->DefaultResponse(Repository::enablePackage($sId, !$bDisable)); + } + + public function DoAdminPluginLoad() : array + { + $this->IsAdminLoggined(); + + $mResult = false; + $sId = (string) $this->GetActionParam('id', ''); + + if (!empty($sId)) { + $oPlugin = $this->Plugins()->CreatePluginByName($sId); + if ($oPlugin) { + $mResult = array( + '@Object' => 'Object/Plugin', + 'id' => $sId, + 'name' => $oPlugin->Name(), + 'readme' => $oPlugin->Description(), + 'config' => array() + ); + + $aMap = $oPlugin->ConfigMap(); + if (\is_array($aMap)) { + $oConfig = $oPlugin->Config(); + foreach ($aMap as $oItem) { + if ($oItem) { + if ($oItem instanceof \RainLoop\Plugins\Property) { + if (PluginPropertyType::PASSWORD === $oItem->Type()) { + $oItem->SetValue(static::APP_DUMMY); + } else { + $oItem->SetValue($oConfig->Get('plugin', $oItem->Name(), '')); + } + $mResult['config'][] = $oItem; + } else if ($oItem instanceof \RainLoop\Plugins\PropertyCollection) { + foreach ($oItem as $oSubItem) { + if ($oSubItem && $oSubItem instanceof \RainLoop\Plugins\Property) { + if (PluginPropertyType::PASSWORD === $oSubItem->Type()) { + $oSubItem->SetValue(static::APP_DUMMY); + } else { + $oSubItem->SetValue($oConfig->Get('plugin', $oSubItem->Name(), '')); + } + } + } + $mResult['config'][] = $oItem; + } + } + } + } + } + } + + return $this->DefaultResponse($mResult); + } + + public function DoAdminPluginSettingsUpdate() : array + { + $this->IsAdminLoggined(); + + $sId = (string) $this->GetActionParam('id', ''); + + if (!empty($sId)) { + $oPlugin = $this->Plugins()->CreatePluginByName($sId); + if ($oPlugin) { + $oConfig = $oPlugin->Config(); + $aMap = $oPlugin->ConfigMap(true); + if (\is_array($aMap)) { + $aSettings = (array) $this->GetActionParam('settings', []); + foreach ($aMap as $oItem) { + $sKey = $oItem->Name(); + $mValue = $aSettings[$sKey] ?? $oConfig->Get('plugin', $sKey); + if (PluginPropertyType::PASSWORD !== $oItem->Type() || static::APP_DUMMY !== $mValue) { + $oItem->SetValue($mValue); + $mValue = $oItem->Value(); + if (null !== $mValue) { + if ($oItem->encrypted) { + $oConfig->setEncrypted('plugin', $sKey, $mValue); + } else { + $oConfig->Set('plugin', $sKey, $mValue); + } + } + } + } + } + if ($oConfig->Save()) { + return $this->TrueResponse(); + } + } + } + + throw new ClientException(Notifications::CantSavePluginSettings); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Attachments.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Attachments.php new file mode 100644 index 000000000..8647b6a0d --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Attachments.php @@ -0,0 +1,175 @@ +GetActionParam('target', ''); + $sFolder = $this->GetActionParam('folder', ''); + $sFilename = \MailSo\Base\Utils::SecureFileName($this->GetActionParam('filename', '')); + $aHashes = $this->GetActionParam('hashes', null); + $oFilesProvider = $this->FilesProvider(); + if (empty($sAction) || !$this->GetCapa(Capa::ATTACHMENTS_ACTIONS) || !$oFilesProvider || !$oFilesProvider->IsActive()) { + return $this->FalseResponse(); + } + + $oAccount = $this->initMailClientConnection(); + + $bError = false; + $aData = []; + $mUIDs = []; + + if (\is_array($aHashes) && \count($aHashes)) { + foreach ($aHashes as $sZipHash) { + $aResult = $this->getMimeFileByHash($oAccount, $sZipHash); + if (empty($aResult['fileHash'])) { + $bError = true; + break; + } + $aData[] = $aResult; + if (!empty($aResult['mimeIndex'])) { + $mUIDs[$aResult['uid']] = $aResult['uid']; + } + } + } + $mUIDs = 1 < \count($mUIDs); + + if ($bError || !\count($aData)) { + return $this->FalseResponse(); + } + + $mResult = false; + switch (\strtolower($sAction)) + { + case 'zip': + + $sZipHash = \MailSo\Base\Utils::Sha1Rand(); + $sZipFileName = $oFilesProvider->GenerateLocalFullFileName($oAccount, $sZipHash); + + if (!empty($sZipFileName)) { + if (\class_exists('ZipArchive')) { + $oZip = new \ZipArchive(); + $oZip->open($sZipFileName, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE); + $oZip->setArchiveComment('SnappyMail/'.APP_VERSION); + foreach ($aData as $aItem) { + $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']); + $sFileName = ($mUIDs ? "{$aItem['uid']}/" : ($sFolder ? "{$aItem['uid']}-" : '')) . $aItem['fileName']; + if (!$oZip->addFile($sFullFileNameHash, $sFileName)) { + $bError = true; + } + } + + if ($bError) { + $oZip->close(); + } else { + $bError = !$oZip->close(); + } +/* + } else { + @\unlink($sZipFileName); + $oZip = new \SnappyMail\Stream\ZIP($sZipFileName); +// $oZip->setArchiveComment('SnappyMail/'.APP_VERSION); + foreach ($aData as $aItem) { + if ($aItem['fileHash']) { + $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']); + if (!$oZip->addFile($sFullFileNameHash, $aItem['fileName'])) { + $bError = true; + } + } + } + $oZip->close(); +*/ + } else { + @\unlink($sZipFileName); + $oZip = new \PharData($sZipFileName . '.zip', 0, null, \Phar::ZIP); + $oZip->compressFiles(\Phar::GZ); + foreach ($aData as $aItem) { + $oZip->addFile( + $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']), + ($mUIDs ? "{$aItem['uid']}/" : ($sFolder ? "{$aItem['uid']}-" : '')) . $aItem['fileName'] + ); + } + $oZip->compressFiles(\Phar::GZ); + unset($oZip); + \rename($sZipFileName . '.zip', $sZipFileName); + } + + foreach ($aData as $aItem) { + $oFilesProvider->Clear($oAccount, $aItem['fileHash']); + } + + if (!$bError) { + $mResult = array( + 'fileHash' => $this->encodeRawKey(array( + 'fileName' => ($sFilename ?: ($sFolder ? 'messages' : 'attachments')) . \date('-YmdHis') . '.zip', + 'mimeType' => 'application/zip', + 'fileHash' => $sZipHash + )) + ); + } + } + break; + + default: + $data = new \SnappyMail\AttachmentsAction; + $data->action = $sAction; + $data->items = $aData; + $data->filesProvider = $oFilesProvider; + $data->account = $oAccount; + $this->Plugins()->RunHook('json.attachments', array($data)); + $mResult = $data->result; + break; + } + + return $this->DefaultResponse($bError ? false : $mResult); + } + + private function getMimeFileByHash(\RainLoop\Model\Account $oAccount, string $sHash) : array + { + $aValues = $this->decodeRawKey($sHash); + + $sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : ''; + $iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0; + $sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : ''; + + $sContentTypeIn = isset($aValues['mimeType']) ? (string) $aValues['mimeType'] : ''; + $sFileNameIn = isset($aValues['fileName']) ? (string) $aValues['fileName'] : 'file.dat'; + + $oFileProvider = $this->FilesProvider(); + + $sResultHash = ''; + + $mResult = $this->MailClient()->MessageMimeStream(function ($rResource, $sContentType, $sFileName, $sMimeIndex = '') + use ($oAccount, $oFileProvider, $sFileNameIn, $sContentTypeIn, &$sResultHash) { + + unset($sContentType, $sFileName, $sMimeIndex); + + if (\is_resource($rResource)) { + $sHash = \MailSo\Base\Utils::Sha1Rand($sFileNameIn.'~'.$sContentTypeIn); + $rTempResource = $oFileProvider->GetFile($oAccount, $sHash, 'wb+'); + + if (\is_resource($rTempResource)) { + if (-1 < \MailSo\Base\Utils::WriteStream($rResource, $rTempResource)) { + $sResultHash = $sHash; + } + + \fclose($rTempResource); + } + } + + }, $sFolder, $iUid, $sMimeIndex); + + $aValues['fileName'] = $sFileNameIn; + $aValues['fileHash'] = $mResult ? $sResultHash : ''; + + return $aValues; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php index 992433a44..f8694232a 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php @@ -10,9 +10,8 @@ trait Contacts $oAccount = $this->getAccountFromToken(); $oAddressBookProvider = $this->AddressBookProvider($oAccount); - if (!$oAddressBookProvider || !$oAddressBookProvider->IsActive()) - { - return $this->FalseResponse(__FUNCTION__); + if (!$oAddressBookProvider || !$oAddressBookProvider->IsActive()) { + return $this->FalseResponse(); } $sPassword = $this->GetActionParam('Password', ''); @@ -22,12 +21,13 @@ trait Contacts $bResult = $this->setContactsSyncData($oAccount, array( 'Mode' => \intval($this->GetActionParam('Mode', '0')), 'User' => $this->GetActionParam('User', ''), - 'Password' => APP_DUMMY === $sPassword && isset($mData['Password']) - ? $mData['Password'] : (APP_DUMMY === $sPassword ? '' : $sPassword), + 'Password' => static::APP_DUMMY === $sPassword + ? (isset($mData['Password']) ? $mData['Password'] : '') + : $sPassword, 'Url' => $this->GetActionParam('Url', '') )); - return $this->DefaultResponse(__FUNCTION__, $bResult); + return $this->DefaultResponse($bResult); } public function DoContactsSync() : array @@ -35,15 +35,15 @@ trait Contacts $oAccount = $this->getAccountFromToken(); $oAddressBookProvider = $this->AddressBookProvider($oAccount); if (!$oAddressBookProvider) { - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'No AddressBookProvider'); } \ignore_user_abort(true); \SnappyMail\HTTP\Stream::start(/*$binary = false*/); \SnappyMail\HTTP\Stream::JSON(['messsage'=>'start']); if (!$oAddressBookProvider->Sync()) { - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'AddressBookProvider->Sync() failed'); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoContacts() : array @@ -60,13 +60,12 @@ trait Contacts $mResult = array(); $oAbp = $this->AddressBookProvider($oAccount); - if ($oAbp->IsActive()) - { + if ($oAbp->IsActive()) { $iResultCount = 0; $mResult = $oAbp->GetContacts($iOffset, $iLimit, $sSearch, $iResultCount); } - return $this->DefaultResponse(__FUNCTION__, array( + return $this->DefaultResponse(array( 'Offset' => $iOffset, 'Limit' => $iLimit, 'Count' => $iResultCount, @@ -78,17 +77,16 @@ trait Contacts public function DoContactsDelete() : array { $oAccount = $this->getAccountFromToken(); - $aUids = \explode(',', (string) $this->GetActionParam('Uids', '')); + $aUids = \explode(',', (string) $this->GetActionParam('uids', '')); $aFilteredUids = \array_filter(\array_map('intval', $aUids)); $bResult = false; - if (\count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive()) - { + if (\count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive()) { $bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($aFilteredUids); } - return $this->DefaultResponse(__FUNCTION__, $bResult); + return $this->DefaultResponse($bResult); } public function DoContactSave() : array @@ -97,14 +95,14 @@ trait Contacts $bResult = false; - if ($this->HasActionParam('Uid') && $this->HasActionParam('jCard')) { + if ($this->HasActionParam('uid') && $this->HasActionParam('jCard')) { $oAddressBookProvider = $this->AddressBookProvider($oAccount); if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { $vCard = \Sabre\VObject\Reader::readJson($this->GetActionParam('jCard')); if ($vCard && $vCard instanceof \Sabre\VObject\Component\VCard) { $vCard->REV = \gmdate('Ymd\\THis\\Z'); $vCard->PRODID = 'SnappyMail-'.APP_VERSION; - $sUid = \trim($this->GetActionParam('Uid')); + $sUid = \trim($this->GetActionParam('uid')); $oContact = $sUid ? $oAddressBookProvider->GetContactByID($sUid) : null; if (!$oContact) { $oContact = new \RainLoop\Providers\AddressBook\Classes\Contact(); @@ -115,75 +113,58 @@ trait Contacts } } - return $this->DefaultResponse(__FUNCTION__, array( + return $this->DefaultResponse(array( 'ResultID' => $bResult ? $oContact->id : '', 'Result' => $bResult )); } - public function UploadContacts() : array + public function UploadContacts(?array $aFile, int $iError) : array { $oAccount = $this->getAccountFromToken(); $mResponse = false; - $aFile = $this->GetActionParam('File', null); - $iError = $this->GetActionParam('Error', \RainLoop\Enumerations\UploadError::UNKNOWN); - - if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) - { + if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { $sSavedName = 'upload-post-'.\md5($aFile['name'].$aFile['tmp_name']); - if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) - { + if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { $iError = \RainLoop\Enumerations\UploadError::ON_SAVING; - } - else - { - \ini_set('auto_detect_line_endings', true); + } else { + \ini_set('auto_detect_line_endings', '1'); $mData = $this->FilesProvider()->GetFile($oAccount, $sSavedName); - if ($mData) - { - $sFileStart = \fread($mData, 20); + if ($mData) { + $sFileStart = \fread($mData, 128); \rewind($mData); - - if (false !== $sFileStart) - { + if (false !== $sFileStart) { $sFileStart = \trim($sFileStart); - if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) - { + if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) { $mResponse = $this->importContactsFromVcfFile($oAccount, $mData); - } - else if (false !== \strpos($sFileStart, ',') || false !== \strpos($sFileStart, ';')) - { + } else if (false !== \strpos($sFileStart, ',') || false !== \strpos($sFileStart, ';')) { $mResponse = $this->importContactsFromCsvFile($oAccount, $mData, $sFileStart); } } } - if (\is_resource($mData)) - { + if (\is_resource($mData)) { \fclose($mData); } unset($mData); $this->FilesProvider()->Clear($oAccount, $sSavedName); - \ini_set('auto_detect_line_endings', false); + \ini_set('auto_detect_line_endings', '0'); } } - if (UPLOAD_ERR_OK !== $iError) - { - $iClientError = \RainLoop\Enumerations\UploadError::NORMAL; - $sError = $this->getUploadErrorMessageByCode($iError, $iClientError); - - if (!empty($sError)) - { - return $this->FalseResponse(__FUNCTION__, $iClientError, $sError); + if (UPLOAD_ERR_OK !== $iError) { + $iClientError = 0; + $sError = \RainLoop\Enumerations\UploadError::getUserMessage($iError, $iClientError); + if (!empty($sError)) { + return $this->FalseResponse($iClientError, $sError); } } - return $this->DefaultResponse(__FUNCTION__, $mResponse); + return $this->DefaultResponse($mResponse); } public function setContactsSyncData(\RainLoop\Model\Account $oAccount, array $aData) : bool @@ -273,64 +254,45 @@ trait Contacts private function importContactsFromVcfFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile): int { $iCount = 0; - if ($oAccount && \is_resource($rFile)) { - $oAddressBookProvider = $this->AddressBookProvider($oAccount); - if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { - $sFile = \stream_get_contents($rFile); - if (\is_resource($rFile)) { - \fclose($rFile); - } - - if (is_string($sFile) && 5 < \strlen($sFile)) { - $this->Logger()->Write('Import contacts from vcf'); - $iCount = $oAddressBookProvider->ImportVcfFile($sFile); + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) { + try + { + $this->logWrite('Import contacts from vcf'); + foreach (\RainLoop\Providers\AddressBook\Utils::VcfStreamToContacts($rFile) as $oContact) { + if ($oAddressBookProvider->ContactSave($oContact)) { + ++$iCount; + } } } + catch (\Throwable $oExc) + { + $this->logException($oExc); + } } - return $iCount; } private function importContactsFromCsvFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile, string $sFileStart): int { $iCount = 0; - $aHeaders = null; - $aData = array(); - - if ($oAccount && \is_resource($rFile)) { - $oAddressBookProvider = $this->AddressBookProvider($oAccount); - if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) { + try + { + $this->logWrite('Import contacts from csv'); $sDelimiter = ((int)\strpos($sFileStart, ',') > (int)\strpos($sFileStart, ';')) ? ',' : ';'; - - \setlocale(LC_CTYPE, 'en_US.UTF-8'); - while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) { - if (null === $aHeaders) { - if (3 >= \count($mRow)) { - return 0; - } - - $aHeaders = $mRow; - - foreach ($aHeaders as $iIndex => $sHeaderValue) { - $aHeaders[$iIndex] = \MailSo\Base\Utils::Utf8Clear($sHeaderValue); - } - } else { - $aNewItem = array(); - foreach ($aHeaders as $iIndex => $sHeaderValue) { - $aNewItem[$sHeaderValue] = isset($mRow[$iIndex]) ? $mRow[$iIndex] : ''; - } - - $aData[] = $aNewItem; + foreach (\RainLoop\Providers\AddressBook\Utils::CsvStreamToContacts($rFile, $sDelimiter) as $oContact) { + if ($oAddressBookProvider->ContactSave($oContact)) { + ++$iCount; } } - - if (\count($aData)) { - $this->oLogger->Write('Import contacts from csv'); - $iCount = $oAddressBookProvider->ImportCsvArray($aData); - } + } + catch (\Throwable $oExc) + { + $this->logException($oExc); } } - return $iCount; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php index ca6de9d67..1d2b02521 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php @@ -6,10 +6,7 @@ use RainLoop\Enumerations\Capa; trait Filters { - /** - * @var \RainLoop\Providers\Filters - */ - private $oFiltersProvider; + private ?\RainLoop\Providers\Filters $oFiltersProvider = null; /** * @throws \MailSo\RuntimeException @@ -18,12 +15,11 @@ trait Filters { $oAccount = $this->getAccountFromToken(); - if (!$this->GetCapa(Capa::SIEVE, $oAccount)) - { - return $this->FalseResponse(__FUNCTION__); + if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { + return $this->FalseResponse(); } - return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Load($oAccount)); + return $this->DefaultResponse($this->FiltersProvider()->Load($oAccount)); } /** @@ -34,7 +30,7 @@ trait Filters $oAccount = $this->getAccountFromToken(); if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } $sName = $this->GetActionParam('name', ''); @@ -43,7 +39,7 @@ trait Filters // $this->FiltersProvider()->ActivateScript($oAccount, $sName); } - return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save( + return $this->DefaultResponse($this->FiltersProvider()->Save( $oAccount, $sName, $this->GetActionParam('body', '') )); } @@ -56,10 +52,10 @@ trait Filters $oAccount = $this->getAccountFromToken(); if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } - return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->ActivateScript( + return $this->DefaultResponse($this->FiltersProvider()->ActivateScript( $oAccount, $this->GetActionParam('name', '') )); } @@ -72,10 +68,10 @@ trait Filters $oAccount = $this->getAccountFromToken(); if (!$this->GetCapa(Capa::SIEVE, $oAccount)) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } - return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->DeleteScript( + return $this->DefaultResponse($this->FiltersProvider()->DeleteScript( $oAccount, $this->GetActionParam('name', '') )); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php index 41323ec88..64d6f6782 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Folders.php @@ -10,44 +10,39 @@ use MailSo\Imap\Enumerations\FolderType; trait Folders { - private function getFolderCollection(bool $HideUnsubscribed) : ?\MailSo\Mail\FolderCollection - { - return $this->MailClient()->Folders('', '*', - $HideUnsubscribed, - (int) $this->Config()->Get('imap', 'folder_list_limit', 200), - (bool) $this->Config()->Get('imap', 'use_list_status', true) - ); - } - /** * Appends uploaded rfc822 message to mailbox * @throws \MailSo\RuntimeException */ - public function Append(): bool + public function DoFolderAppend(): array { $oAccount = $this->initMailClientConnection(); - $sFolderFullName = $this->GetActionParam('Folder', ''); + $sFolderFullName = $this->GetActionParam('folder', ''); - if ($oAccount - && !empty($sFolderFullName) - && !empty($_FILES['AppendFile']) - && \is_uploaded_file($_FILES['AppendFile']['tmp_name']) - && \UPLOAD_ERR_OK == $_FILES['AppendFile']['error'] - && $this->oConfig->Get('labs', 'allow_message_append', false) - ) { - $sSavedName = 'append-post-' . \md5($sFolderFullName . $_FILES['AppendFile']['name'] . $_FILES['AppendFile']['tmp_name']); - if ($this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $_FILES['AppendFile']['tmp_name'])) { - $iMessageStreamSize = $this->FilesProvider()->FileSize($oAccount, $sSavedName); - $rMessageStream = $this->FilesProvider()->GetFile($oAccount, $sSavedName); - - $this->MailClient()->MessageAppendStream($rMessageStream, $iMessageStreamSize, $sFolderFullName); - - $this->FilesProvider()->Clear($oAccount, $sSavedName); - } + if (!$this->oConfig->Get('labs', 'allow_message_append', false)) { + return $this->FalseResponse(999, 'Permission denied'); } - return $this->DefaultResponse(__FUNCTION__, true); + if (empty($_FILES['appendFile'])) { + return $this->FalseResponse(999, 'No file'); + } + + if (\UPLOAD_ERR_OK != $_FILES['appendFile']['error']) { + return $this->FalseResponse($iErrorCode, \RainLoop\Enumerations\UploadError::getMessage($iErrorCode)); + } + + if ($oAccount && !empty($sFolderFullName) && \is_uploaded_file($_FILES['appendFile']['tmp_name'])) { + $sSavedName = 'append-post-' . \md5($sFolderFullName . $_FILES['appendFile']['name'] . $_FILES['appendFile']['tmp_name']); + if ($this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $_FILES['appendFile']['tmp_name'])) { + $iMessageStreamSize = $this->FilesProvider()->FileSize($oAccount, $sSavedName); + $rMessageStream = $this->FilesProvider()->GetFile($oAccount, $sSavedName); + $this->ImapClient()->MessageAppendStream($sFolderFullName, $rMessageStream, $iMessageStreamSize); + $this->FilesProvider()->Clear($oAccount, $sSavedName); + return $this->TrueResponse(); + } + } + return $this->FalseResponse(999); } public function DoFolders() : array @@ -60,159 +55,35 @@ trait Folders $HideUnsubscribed = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $HideUnsubscribed); } - $oFolderCollection = $this->getFolderCollection($HideUnsubscribed); + $oFolderCollection = $this->MailClient()->Folders('', '*', $HideUnsubscribed); - if ($oFolderCollection) - { - $sNamespace = $this->MailClient()->GetPersonalNamespace(); - - $this->Plugins()->RunHook('filter.folders-post', array($oAccount, $oFolderCollection)); - - $aSystemFolders = array(); - $this->recFoldersTypes($oAccount, $oFolderCollection, $aSystemFolders); - - if (!$this->Config()->Get('imap', 'use_sort', true)) { - $oFolderCollection->capabilities = \array_filter($oFolderCollection->capabilities, function($item){ - return !\preg_match('/^E?SORT/', $item); - }); - } - - if ($this->Config()->Get('labs', 'autocreate_system_folders', false)) - { - $bDoItAgain = false; - - $sParent = \substr($sNamespace, 0, -1); - - $sDelimiter = $oFolderCollection->FindDelimiter(); - - $aList = array(); - $aMap = $this->systemFoldersNames($oAccount); - - if ('' === $oSettingsLocal->GetConf('SentFolder', '')) - { - $aList[] = FolderType::SENT; - } - - if ('' === $oSettingsLocal->GetConf('DraftFolder', '')) - { - $aList[] = FolderType::DRAFTS; - } - - if ('' === $oSettingsLocal->GetConf('SpamFolder', '')) - { - $aList[] = FolderType::JUNK; - } - - if ('' === $oSettingsLocal->GetConf('TrashFolder', '')) - { - $aList[] = FolderType::TRASH; - } - - if ('' === $oSettingsLocal->GetConf('ArchiveFolder', '')) - { - $aList[] = FolderType::ARCHIVE; - } - - $this->Plugins()->RunHook('filter.folders-system-types', array($oAccount, &$aList)); - - foreach ($aList as $iType) - { - if (!isset($aSystemFolders[$iType])) - { - $mFolderNameToCreate = \array_search($iType, $aMap); - if (!empty($mFolderNameToCreate)) - { - $iPos = \strrpos($mFolderNameToCreate, $sDelimiter); - if (false !== $iPos) - { - $mNewParent = \substr($mFolderNameToCreate, 0, $iPos); - $mNewFolderNameToCreate = \substr($mFolderNameToCreate, $iPos + 1); - if (\strlen($mNewFolderNameToCreate)) - { - $mFolderNameToCreate = $mNewFolderNameToCreate; - } - - if (\strlen($mNewParent)) - { - $sParent = \strlen($sParent) ? $sParent.$sDelimiter.$mNewParent : $mNewParent; - } - } - - $sFullNameToCheck = $mFolderNameToCreate; - if (\strlen($sParent)) - { - $sFullNameToCheck = $sParent.$sDelimiter.$sFullNameToCheck; - } - - if (!isset($oFolderCollection[$sFullNameToCheck])) - { - try - { - $this->MailClient()->FolderCreate($mFolderNameToCreate, $sParent, true, $sDelimiter); - $bDoItAgain = true; - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException); - } - } - } - } - } - - if ($bDoItAgain) - { - $oFolderCollection = $this->getFolderCollection($HideUnsubscribed); - - if ($oFolderCollection) - { - $aSystemFolders = array(); - $this->recFoldersTypes($oAccount, $oFolderCollection, $aSystemFolders); - } + if ($oFolderCollection) { + $aQuota = null; + if ($this->GetCapa(Capa::QUOTA)) { + try { +// $aQuota = $this->ImapClient()->Quota(); + $aQuota = $this->ImapClient()->QuotaRoot(); + } catch (\Throwable $oException) { + // ignore } } - if ($oFolderCollection) - { - $this->Plugins()->RunHook('filter.folders-complete', array($oAccount, $oFolderCollection)); + $aCapabilities = \array_values(\array_filter($this->ImapClient()->Capability(), function ($item) { + return !\preg_match('/^(IMAP|AUTH|LOGIN|SASL)/', $item); + })); - $aQuota = null; - if ($this->GetCapa(Capa::QUOTA)) { - try { -// $aQuota = $this->MailClient()->Quota(); - $aQuota = $this->MailClient()->QuotaRoot(); - } catch (\Throwable $oException) { - // ignore - } - } - - $aCapabilities = \array_filter($this->MailClient()->Capability(), function($item){ - return !\preg_match('/^(IMAP|AUTH|LOGIN|SASL)/', $item); - }); - if (!$this->Config()->Get('imap', 'use_list_status', true)) { - $key = \array_search('LIST-STATUS', $aCapabilities); - if (false !== $key) { - unset($aCapabilities[$key]); - } - } - - $oFolderCollection = \array_merge( - $oFolderCollection->jsonSerialize(), - array( - 'quotaUsage' => $aQuota ? $aQuota[0] * 1024 : null, - 'quotaLimit' => $aQuota ? $aQuota[1] * 1024 : null, - 'Namespace' => $sNamespace, - 'IsThreadsSupported' => $this->MailClient()->IsThreadsSupported(), - 'Optimized' => $oFolderCollection->Optimized, - 'CountRec' => $oFolderCollection->TotalCount, - 'SystemFolders' => empty($aSystemFolders) ? null : $aSystemFolders, - 'Capabilities' => \array_values($aCapabilities) - ) - ); - } + $oFolderCollection = \array_merge( + $oFolderCollection->jsonSerialize(), + array( + 'quotaUsage' => $aQuota ? $aQuota[0] * 1024 : null, + 'quotaLimit' => $aQuota ? $aQuota[1] * 1024 : null, + 'namespace' => $this->ImapClient()->GetPersonalNamespace(), + 'capabilities' => $aCapabilities + ) + ); } - return $this->DefaultResponse(__FUNCTION__, $oFolderCollection); + return $this->DefaultResponse($oFolderCollection); } public function DoFolderCreate() : array @@ -222,13 +93,13 @@ trait Folders try { $oFolder = $this->MailClient()->FolderCreate( - $this->GetActionParam('Folder', ''), - $this->GetActionParam('Parent', ''), - !!$this->GetActionParam('Subscribe', 1) + $this->GetActionParam('folder', ''), + $this->GetActionParam('parent', ''), + !empty($this->GetActionParam('subscribe', 0)) ); // FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array()) - return $this->DefaultResponse(__FUNCTION__, $oFolder); + return $this->DefaultResponse($oFolder); } catch (\Throwable $oException) { @@ -239,82 +110,60 @@ trait Folders public function DoFolderSetMetadata() : array { $this->initMailClientConnection(); - $sFolderFullName = $this->GetActionParam('Folder'); - $sMetadataKey = $this->GetActionParam('Key'); + $sFolderFullName = $this->GetActionParam('folder'); + $sMetadataKey = $this->GetActionParam('key'); if ($sFolderFullName && $sMetadataKey) { - $this->MailClient()->FolderSetMetadata($sFolderFullName, [ - $sMetadataKey => $this->GetActionParam('Value') ?: null + $this->ImapClient()->FolderSetMetadata($sFolderFullName, [ + $sMetadataKey => $this->GetActionParam('value') ?: null ]); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoFolderSubscribe() : array { $this->initMailClientConnection(); - $sFolderFullName = $this->GetActionParam('Folder', ''); - $bSubscribe = '1' === (string) $this->GetActionParam('Subscribe', '0'); + $sFolderFullName = $this->GetActionParam('folder', ''); + $bSubscribe = !empty($this->GetActionParam('subscribe', 0)); try { - $this->MailClient()->FolderSubscribe($sFolderFullName, $bSubscribe); + $this->ImapClient()->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName); } catch (\Throwable $oException) { - if ($bSubscribe) - { - throw new ClientException(Notifications::CantSubscribeFolder, $oException); - } - else - { - throw new ClientException(Notifications::CantUnsubscribeFolder, $oException); - } + throw new ClientException( + $bSubscribe ? Notifications::CantSubscribeFolder : Notifications::CantUnsubscribeFolder, + $oException + ); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoFolderCheckable() : array { $oAccount = $this->getAccountFromToken(); - $sFolderFullName = $this->GetActionParam('Folder', ''); - $bCheckable = '1' === (string) $this->GetActionParam('Checkable', '0'); + $sFolderFullName = $this->GetActionParam('folder', ''); $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); - $sCheckableFolder = $oSettingsLocal->GetConf('CheckableFolder', '[]'); - $aCheckableFolder = \json_decode($sCheckableFolder); - - if (!\is_array($aCheckableFolder)) - { + $aCheckableFolder = \json_decode($oSettingsLocal->GetConf('CheckableFolder', '[]')); + if (!\is_array($aCheckableFolder)) { $aCheckableFolder = array(); } - if ($bCheckable) - { + if (!empty($this->GetActionParam('checkable', '0'))) { $aCheckableFolder[] = $sFolderFullName; - } - else - { - $aCheckableFolderNew = array(); - foreach ($aCheckableFolder as $sFolder) - { - if ($sFolder !== $sFolderFullName) - { - $aCheckableFolderNew[] = $sFolder; - } - } - $aCheckableFolder = $aCheckableFolderNew; + } else if (($key = \array_search($sFolderFullName, $aCheckableFolder)) !== false) { + \array_splice($aCheckableFolder, $key, 1); } - $aCheckableFolder = \array_unique($aCheckableFolder); + $oSettingsLocal->SetConf('CheckableFolder', \json_encode(\array_unique($aCheckableFolder))); - $oSettingsLocal->SetConf('CheckableFolder', \json_encode($aCheckableFolder)); - - return $this->DefaultResponse(__FUNCTION__, - $this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal)); + return $this->DefaultResponse($this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal)); } /** @@ -327,9 +176,9 @@ trait Folders try { $this->MailClient()->FolderMove( - $this->GetActionParam('Folder', ''), - $this->GetActionParam('NewFolder', ''), - !!$this->GetActionParam('Subscribe', 1) + $this->GetActionParam('folder', ''), + $this->GetActionParam('newFolder', ''), + !empty($this->GetActionParam('subscribe', 1)) ); } catch (\Throwable $oException) @@ -337,7 +186,7 @@ trait Folders throw new ClientException(Notifications::CantRenameFolder, $oException); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } /** @@ -347,13 +196,13 @@ trait Folders { $this->initMailClientConnection(); - $sName = $this->GetActionParam('NewFolderName', ''); + $sName = $this->GetActionParam('newFolderName', ''); try { $sFullName = $this->MailClient()->FolderRename( - $this->GetActionParam('Folder', ''), + $this->GetActionParam('folder', ''), $sName, - !!$this->GetActionParam('Subscribe', 1) + !empty($this->GetActionParam('subscribe', 1)) ); } catch (\Throwable $oException) @@ -362,9 +211,9 @@ trait Folders } // FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array()) - return $this->DefaultResponse(__FUNCTION__, array( - 'Name' => $sName, - 'FullName' => $sFullName, + return $this->DefaultResponse(array( + 'name' => $sName, + 'fullName' => $sFullName, )); } @@ -377,7 +226,7 @@ trait Folders try { - $this->MailClient()->FolderDelete($this->GetActionParam('Folder', '')); + $this->ImapClient()->FolderDelete($this->GetActionParam('folder', '')); } catch (\MailSo\Mail\Exceptions\NonEmptyFolder $oException) { @@ -388,7 +237,7 @@ trait Folders throw new ClientException(Notifications::CantDeleteFolder, $oException); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } /** @@ -400,14 +249,14 @@ trait Folders try { - $this->MailClient()->FolderClear($this->GetActionParam('Folder', '')); + $this->ImapClient()->FolderClear($this->GetActionParam('folder', '')); } catch (\Throwable $oException) { throw new ClientException(Notifications::MailServerError, $oException); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } /** @@ -419,12 +268,11 @@ trait Folders try { - $aInboxInformation = $this->MailClient()->FolderInformation( - $this->GetActionParam('Folder', ''), - (int) $this->GetActionParam('UidNext', 0), - new \MailSo\Imap\SequenceSet($this->GetActionParam('FlagsUids', [])) - ); - return $this->DefaultResponse(__FUNCTION__, $aInboxInformation); + return $this->DefaultResponse($this->MailClient()->FolderInformation( + $this->GetActionParam('folder', ''), + (int) $this->GetActionParam('uidNext', 0), + new \MailSo\Imap\SequenceSet($this->GetActionParam('flagsUids', [])) + )); } catch (\Throwable $oException) { @@ -439,38 +287,24 @@ trait Folders { $aResult = array(); - $aFolders = $this->GetActionParam('Folders', null); - if (\is_array($aFolders)) - { + $aFolders = $this->GetActionParam('folders', null); + if (\is_array($aFolders)) { $this->initMailClientConnection(); $aFolders = \array_unique($aFolders); - foreach ($aFolders as $sFolder) - { - if (\strlen($sFolder) && 'INBOX' !== \strtoupper($sFolder)) + foreach ($aFolders as $sFolder) { + try { - try - { - $aInboxInformation = $this->MailClient()->FolderInformation($sFolder); - if (isset($aInboxInformation['Folder'])) - { - $aResult[] = [ - 'Folder' => $aInboxInformation['Folder'], - 'Hash' => $aInboxInformation['Hash'], - 'totalEmails' => $aInboxInformation['totalEmails'], - 'unreadEmails' => $aInboxInformation['unreadEmails'], - ]; - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException); - } + $aResult[] = $this->MailClient()->FolderInformation($sFolder); + } + catch (\Throwable $oException) + { + $this->logException($oException); } } } - return $this->DefaultResponse(__FUNCTION__, $aResult); + return $this->DefaultResponse($aResult); } public function DoSystemFoldersUpdate() : array @@ -479,132 +313,12 @@ trait Folders $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); - $oSettingsLocal->SetConf('SentFolder', $this->GetActionParam('Sent', '')); - $oSettingsLocal->SetConf('DraftFolder', $this->GetActionParam('Drafts', '')); - $oSettingsLocal->SetConf('SpamFolder', $this->GetActionParam('Spam', '')); - $oSettingsLocal->SetConf('TrashFolder', $this->GetActionParam('Trash', '')); - $oSettingsLocal->SetConf('ArchiveFolder', $this->GetActionParam('Archive', '')); + $oSettingsLocal->SetConf('SentFolder', $this->GetActionParam('sent', '')); + $oSettingsLocal->SetConf('DraftsFolder', $this->GetActionParam('drafts', '')); + $oSettingsLocal->SetConf('JunkFolder', $this->GetActionParam('junk', '')); + $oSettingsLocal->SetConf('TrashFolder', $this->GetActionParam('trash', '')); + $oSettingsLocal->SetConf('ArchiveFolder', $this->GetActionParam('archive', '')); - return $this->DefaultResponse(__FUNCTION__, - $this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal)); - } - - private function recFoldersTypes(\RainLoop\Model\Account $oAccount, \MailSo\Mail\FolderCollection $oFolders, array &$aResult, bool $bListFolderTypes = true) : void - { - if ($oFolders->Count()) - { - if ($bListFolderTypes) - { - foreach ($oFolders as $oFolder) - { - $iFolderType = $oFolder->GetFolderListType(); - if (!isset($aResult[$iFolderType]) && \in_array($iFolderType, array( - FolderType::INBOX, - FolderType::SENT, - FolderType::DRAFTS, - FolderType::JUNK, - FolderType::TRASH, - FolderType::ARCHIVE - ))) - { - $aResult[$iFolderType] = $oFolder->FullName(); - } - } - } - - $aMap = $this->systemFoldersNames($oAccount); - foreach ($oFolders as $oFolder) - { - $sName = $oFolder->Name(); - $sFullName = $oFolder->FullName(); - - if (isset($aMap[$sName]) || isset($aMap[$sFullName])) - { - $iFolderType = isset($aMap[$sName]) ? $aMap[$sName] : $aMap[$sFullName]; - if ((!isset($aResult[$iFolderType]) || $sName === $sFullName || "INBOX{$oFolder->Delimiter()}{$sName}" === $sFullName) && \in_array($iFolderType, array( - FolderType::INBOX, - FolderType::SENT, - FolderType::DRAFTS, - FolderType::JUNK, - FolderType::TRASH, - FolderType::ARCHIVE - ))) - { - $aResult[$iFolderType] = $oFolder->FullName(); - } - } - } - } - } - - /** - * @staticvar array $aCache - */ - private function systemFoldersNames(\RainLoop\Model\Account $oAccount) : array - { - static $aCache = null; - if (null === $aCache) - { - $aCache = array( - - 'Sent' => FolderType::SENT, - 'Send' => FolderType::SENT, - - 'Outbox' => FolderType::SENT, - 'Out box' => FolderType::SENT, - - 'Sent Item' => FolderType::SENT, - 'Sent Items' => FolderType::SENT, - 'Send Item' => FolderType::SENT, - 'Send Items' => FolderType::SENT, - 'Sent Mail' => FolderType::SENT, - 'Sent Mails' => FolderType::SENT, - 'Send Mail' => FolderType::SENT, - 'Send Mails' => FolderType::SENT, - - 'Drafts' => FolderType::DRAFTS, - - 'Draft' => FolderType::DRAFTS, - 'Draft Mail' => FolderType::DRAFTS, - 'Draft Mails' => FolderType::DRAFTS, - 'Drafts Mail' => FolderType::DRAFTS, - 'Drafts Mails' => FolderType::DRAFTS, - - 'Junk E-mail' => FolderType::JUNK, - - 'Spam' => FolderType::JUNK, - 'Spams' => FolderType::JUNK, - - 'Junk' => FolderType::JUNK, - 'Bulk Mail' => FolderType::JUNK, - 'Bulk Mails' => FolderType::JUNK, - - 'Deleted Items' => FolderType::TRASH, - - 'Trash' => FolderType::TRASH, - 'Deleted' => FolderType::TRASH, - 'Bin' => FolderType::TRASH, - - 'Archive' => FolderType::ARCHIVE, - 'Archives' => FolderType::ARCHIVE, - - 'All' => FolderType::ALL, - 'All Mail' => FolderType::ALL, - 'All Mails' => FolderType::ALL, - ); - - $aNewCache = array(); - foreach ($aCache as $sKey => $iType) - { - $aNewCache[$sKey] = $iType; - $aNewCache[\str_replace(' ', '', $sKey)] = $iType; - } - - $aCache = $aNewCache; - - $this->Plugins()->RunHook('filter.system-folders-names', array($oAccount, &$aCache)); - } - - return $aCache; + return $this->DefaultResponse($this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal)); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php index 58a979b97..d31974601 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php @@ -14,7 +14,7 @@ trait Localization if ($oAccount = $this->getAccountFromToken(false)) { if ($oConfig->Get('webmail', 'allow_languages_on_settings', true) && ($oSettings = $this->SettingsProvider()->Load($oAccount))) { - $sLanguage = $oSettings->GetConf('Language', $sLanguage); + $sLanguage = $oSettings->GetConf('language', $sLanguage); } } else if ($oConfig->Get('login', 'allow_languages_on_login', true) && $oConfig->Get('login', 'determine_user_language', true)) { $sLanguage = $this->ValidateLanguage($this->detectUserLanguage($bAdmin), $sLanguage, false); @@ -69,7 +69,7 @@ trait Localization private function getUserLanguagesFromHeader(): array { $aResult = $aList = array(); - $sAcceptLang = \strtolower($this->Http()->GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); + $sAcceptLang = \strtolower(\MailSo\Base\Http::GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); if (!empty($sAcceptLang) && \preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $sAcceptLang, $aList)) { $aResult = \array_combine($aList[1], $aList[2]); foreach ($aResult as $n => $v) { 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 507ef448d..0dfb90146 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 @@ -10,6 +10,7 @@ use MailSo\Imap\SequenceSet; use MailSo\Imap\Enumerations\FetchType; use MailSo\Imap\Enumerations\MessageFlag; use MailSo\Mime\Part as MimePart; +use MailSo\Mime\Enumerations\Header as MimeEnumHeader; trait Messages { @@ -22,83 +23,86 @@ trait Messages // throw new ClientException(Notifications::CantGetMessageList); $oParams = new \MailSo\Mail\MessageListParams; - $sRawKey = $this->GetActionParam('RawKey', ''); - $aValues = \json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($sRawKey), true); - if ($aValues && 6 < \count($aValues)) - { - $this->verifyCacheByKey($sRawKey); - -// $oParams->sHash = (string) $aValues['Hash']; - $oParams->sFolderName = (string) $aValues['Folder']; - $oParams->iLimit = $aValues['Limit']; - $oParams->iOffset = $aValues['Offset']; - $oParams->sSearch = (string) $aValues['Search']; - $oParams->sSort = (string) $aValues['Sort']; - if (isset($aValues['UidNext'])) { - $oParams->iPrevUidNext = $aValues['UidNext']; + $aValues = $this->decodeRawKey($this->GetActionParam('RawKey', '')); + $sHash = ''; + if ($aValues && 6 < \count($aValues)) { + // GET + $sHash = (string) $aValues['hash']; + $oParams->sFolderName = (string) $aValues['folder']; + $oParams->iLimit = $aValues['limit']; + $oParams->iOffset = $aValues['offset']; + $oParams->sSearch = (string) $aValues['search']; + $oParams->sSort = (string) $aValues['sort']; + if (isset($aValues['uidNext'])) { + $oParams->iPrevUidNext = $aValues['uidNext']; } - $oParams->bUseThreads = !empty($aValues['UseThreads']); - if ($oParams->bUseThreads && isset($aValues['ThreadUid'])) { - $oParams->iThreadUid = $aValues['ThreadUid']; + $oParams->bUseThreads = !empty($aValues['useThreads']); + if ($oParams->bUseThreads && isset($aValues['threadUid'])) { + $oParams->iThreadUid = $aValues['threadUid']; } - } - else - { - $oParams->sFolderName = $this->GetActionParam('Folder', ''); - $oParams->iOffset = $this->GetActionParam('Offset', 0); - $oParams->iLimit = $this->GetActionParam('Limit', 10); - $oParams->sSearch = $this->GetActionParam('Search', ''); - $oParams->sSort = $this->GetActionParam('Sort', ''); - $oParams->iPrevUidNext = $this->GetActionParam('UidNext', 0); - $oParams->bUseThreads = !empty($this->GetActionParam('UseThreads', '0')); + } else { + // POST + $oParams->sFolderName = $this->GetActionParam('folder', ''); + $oParams->iOffset = $this->GetActionParam('offset', 0); + $oParams->iLimit = $this->GetActionParam('limit', 10); + $oParams->sSearch = $this->GetActionParam('search', ''); + $oParams->sSort = $this->GetActionParam('sort', ''); + $oParams->iPrevUidNext = $this->GetActionParam('uidNext', 0); + $oParams->bUseThreads = !empty($this->GetActionParam('useThreads', '0')); if ($oParams->bUseThreads) { - $oParams->iThreadUid = $this->GetActionParam('ThreadUid', ''); + $oParams->iThreadUid = $this->GetActionParam('threadUid', ''); } } - if (!\strlen($oParams->sFolderName)) - { + if (!\strlen($oParams->sFolderName)) { throw new ClientException(Notifications::CantGetMessageList); } $oAccount = $this->initMailClientConnection(); + if ($sHash) { +// $oInfo = $this->MailClient()->FolderHash($oParams->sFolderName); + $oInfo = $this->ImapClient()->FolderStatusAndSelect($oParams->sFolderName); + $aRequestHash = \explode('-', $sHash); + $sFolderHash = $oInfo->etag; + $sHash = $oParams->hash() . '-' . $sFolderHash; + if ($aRequestHash[1] == $sFolderHash) { + $this->verifyCacheByKey($sHash); + } + } + try { - if (!$this->Config()->Get('imap', 'use_thread', false)) { - $oParams->bUseThreads = false; + if ($this->Config()->Get('cache', 'enable', true) && $this->Config()->Get('cache', 'server_uids', false)) { + $oParams->oCacher = $this->Cacher($oAccount); } - $oParams->oCacher = $this->cacherForUids(); - $oParams->bUseSortIfSupported = !!$this->Config()->Get('imap', 'use_sort', true); + $oParams->bUseSort = true; $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); if ($oSettingsLocal instanceof \RainLoop\Settings) { $oParams->bHideDeleted = !empty($oSettingsLocal->GetConf('HideDeleted', 1)); } +// \ignore_user_abort(true); $oMessageList = $this->MailClient()->MessageList($oParams); + if ($sHash) { + $this->cacheByKey($sHash); + } + return $this->DefaultResponse($oMessageList); } catch (\Throwable $oException) { throw new ClientException(Notifications::CantGetMessageList, $oException); } - - if ($oMessageList) - { - $this->cacheByKey($sRawKey); - } - - return $this->DefaultResponse(__FUNCTION__, $oMessageList); } public function DoSaveMessage() : array { $oAccount = $this->initMailClientConnection(); - $sDraftFolder = $this->GetActionParam('SaveFolder', ''); - if (!\strlen($sDraftFolder)) - { + $sDraftFolder = $this->GetActionParam('saveFolder', ''); + if (!\strlen($sDraftFolder)) { throw new ClientException(Notifications::UnknownError); } @@ -107,49 +111,42 @@ trait Messages $this->Plugins()->RunHook('filter.save-message', array($oMessage)); $mResult = false; - if ($oMessage) - { + if ($oMessage) { $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); - $iMessageStreamSize = \MailSo\Base\Utils::MultipleStreamWriter( - $oMessage->ToStream(false), array($rMessageStream), 8192, true, true); + $iMessageStreamSize = \MailSo\Base\Utils::WriteStream($oMessage->ToStream(false), $rMessageStream, 8192, true); - if (false !== $iMessageStreamSize) - { + if (false !== $iMessageStreamSize) { $sMessageId = $oMessage->MessageId(); \rewind($rMessageStream); - $iNewUid = 0; - $this->MailClient()->MessageAppendStream( - $rMessageStream, $iMessageStreamSize, $sDraftFolder, array(MessageFlag::SEEN), $iNewUid + $iNewUid = $this->ImapClient()->MessageAppendStream( + $sDraftFolder, $rMessageStream, $iMessageStreamSize, array(MessageFlag::SEEN) ); - if (!empty($sMessageId) && (null === $iNewUid || 0 === $iNewUid)) - { + if (!empty($sMessageId) && (null === $iNewUid || 0 === $iNewUid)) { $iNewUid = $this->MailClient()->FindMessageUidByMessageId($sDraftFolder, $sMessageId); } $mResult = true; - $sMessageFolder = $this->GetActionParam('MessageFolder', ''); - $iMessageUid = (int) $this->GetActionParam('MessageUid', 0); - if (\strlen($sMessageFolder) && 0 < $iMessageUid) - { - $this->MailClient()->MessageDelete($sMessageFolder, new SequenceSet($iMessageUid)); + $sMessageFolder = $this->GetActionParam('messageFolder', ''); + $iMessageUid = (int) $this->GetActionParam('messageUid', 0); + if (\strlen($sMessageFolder) && 0 < $iMessageUid) { + $this->ImapClient()->MessageDelete($sMessageFolder, new SequenceSet($iMessageUid)); } - if (null !== $iNewUid && 0 < $iNewUid) - { + if (null !== $iNewUid && 0 < $iNewUid) { $mResult = array( - 'NewFolder' => $sDraftFolder, - 'NewUid' => $iNewUid + 'folder' => $sDraftFolder, + 'uid' => $iNewUid ); } } } - return $this->DefaultResponse(__FUNCTION__, $mResult); + return $this->DefaultResponse($mResult); } public function DoSendMessage() : array @@ -158,8 +155,8 @@ trait Messages $oConfig = $this->Config(); - $sSentFolder = $this->GetActionParam('SaveFolder', ''); - $aDraftInfo = $this->GetActionParam('DraftInfo', null); + $sSentFolder = $this->GetActionParam('saveFolder', ''); + $aDraftInfo = $this->GetActionParam('draftInfo', null); $oMessage = $this->buildMessage($oAccount, false); @@ -168,20 +165,19 @@ trait Messages $mResult = false; try { - if ($oMessage) - { + if ($oMessage) { $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); - $iMessageStreamSize = \MailSo\Base\Utils::MultipleStreamWriter( - $oMessage->ToStream(true), array($rMessageStream), 8192, true, true, true); + $iMessageStreamSize = \MailSo\Base\Utils::WriteStream( + $oMessage->ToStream(true), $rMessageStream, 8192, true, true + ); - if (false !== $iMessageStreamSize) - { - $bDsn = !empty($this->GetActionParam('Dsn', 0)); - $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, $bDsn, true); + if (false !== $iMessageStreamSize) { + $bDsn = !empty($this->GetActionParam('dsn', 0)); + $bRequireTLS = !empty($this->GetActionParam('requireTLS', 0)); + $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, true, $bDsn, $bRequireTLS); - if (\is_array($aDraftInfo) && 3 === \count($aDraftInfo)) - { + if (\is_array($aDraftInfo) && 3 === \count($aDraftInfo)) { $sDraftInfoType = $aDraftInfo[0]; $iDraftInfoUid = (int) $aDraftInfo[1]; $sDraftInfoFolder = $aDraftInfo[2]; @@ -201,44 +197,39 @@ trait Messages } catch (\Throwable $oException) { - $this->Logger()->WriteException($oException, \LOG_ERR); + $this->logException($oException, \LOG_ERR); } } - if (\strlen($sSentFolder)) - { + if (\strlen($sSentFolder)) { try { - if (!$oMessage->GetBcc()) - { - if (\is_resource($rMessageStream)) - { + if (!$oMessage->GetBcc()) { + if (\is_resource($rMessageStream)) { \rewind($rMessageStream); } $this->Plugins()->RunHook('filter.send-message-stream', array($oAccount, &$rMessageStream, &$iMessageStreamSize)); - $this->MailClient()->MessageAppendStream( - $rMessageStream, $iMessageStreamSize, $sSentFolder, array(MessageFlag::SEEN) + $this->ImapClient()->MessageAppendStream( + $sSentFolder, $rMessageStream, $iMessageStreamSize, array(MessageFlag::SEEN) ); - } - else - { + } else { $rAppendMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); - $iAppendMessageStreamSize = \MailSo\Base\Utils::MultipleStreamWriter( - $oMessage->ToStream(false), array($rAppendMessageStream), 8192, true, true, true); + $iAppendMessageStreamSize = \MailSo\Base\Utils::WriteStream( + $oMessage->ToStream(false), $rAppendMessageStream, 8192, true, true + ); $this->Plugins()->RunHook('filter.send-message-stream', array($oAccount, &$rAppendMessageStream, &$iAppendMessageStreamSize)); - $this->MailClient()->MessageAppendStream( - $rAppendMessageStream, $iAppendMessageStreamSize, $sSentFolder, array(MessageFlag::SEEN) + $this->ImapClient()->MessageAppendStream( + $sSentFolder, $rAppendMessageStream, $iAppendMessageStreamSize, array(MessageFlag::SEEN) ); - if (\is_resource($rAppendMessageStream)) - { + if (\is_resource($rAppendMessageStream)) { fclose($rAppendMessageStream); } } @@ -249,24 +240,22 @@ trait Messages } } - if (\is_resource($rMessageStream)) - { + if (\is_resource($rMessageStream)) { \fclose($rMessageStream); } $this->deleteMessageAttachments($oAccount); - $sDraftFolder = $this->GetActionParam('MessageFolder', ''); - $iDraftUid = (int) $this->GetActionParam('MessageUid', 0); - if (\strlen($sDraftFolder) && 0 < $iDraftUid) - { + $sDraftFolder = $this->GetActionParam('messageFolder', ''); + $iDraftUid = (int) $this->GetActionParam('messageUid', 0); + if (\strlen($sDraftFolder) && 0 < $iDraftUid) { try { - $this->MailClient()->MessageDelete($sDraftFolder, new SequenceSet($iDraftUid)); + $this->ImapClient()->MessageDelete($sDraftFolder, new SequenceSet($iDraftUid)); } catch (\Throwable $oException) { - $this->Logger()->WriteException($oException, \LOG_ERR); + $this->logException($oException, \LOG_ERR); } } @@ -283,27 +272,22 @@ trait Messages throw new ClientException(Notifications::CantSendMessage, $oException); } - if (false === $mResult) - { + if (false === $mResult) { throw new ClientException(Notifications::CantSendMessage); } try { - if ($oMessage && $this->AddressBookProvider($oAccount)->IsActive()) - { + if ($oMessage && $this->AddressBookProvider($oAccount)->IsActive()) { $aArrayToFrec = array(); $oToCollection = $oMessage->GetTo(); - if ($oToCollection) - { - foreach ($oToCollection as /* @var $oEmail \MailSo\Mime\Email */ $oEmail) - { + if ($oToCollection) { + foreach ($oToCollection as /* @var $oEmail \MailSo\Mime\Email */ $oEmail) { $aArrayToFrec[$oEmail->GetEmail(true)] = $oEmail->ToString(false, true); } } - if (\count($aArrayToFrec)) - { + if (\count($aArrayToFrec)) { $oSettings = $this->SettingsProvider()->Load($oAccount); $this->AddressBookProvider($oAccount)->IncFrec( @@ -315,10 +299,10 @@ trait Messages } catch (\Throwable $oException) { - $this->Logger()->WriteException($oException); + $this->logException($oException); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoSendReadReceiptMessage() : array @@ -332,31 +316,28 @@ trait Messages $mResult = false; try { - if ($oMessage) - { + if ($oMessage) { $rMessageStream = \MailSo\Base\ResourceRegistry::CreateMemoryResource(); - $iMessageStreamSize = \MailSo\Base\Utils::MultipleStreamWriter( - $oMessage->ToStream(true), array($rMessageStream), 8192, true, true, true); + $iMessageStreamSize = \MailSo\Base\Utils::WriteStream( + $oMessage->ToStream(true), $rMessageStream, 8192, true, true + ); - if (false !== $iMessageStreamSize) - { - $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, false, false); + if (false !== $iMessageStreamSize) { + $this->smtpSendMessage($oAccount, $oMessage, $rMessageStream, $iMessageStreamSize, false); - if (\is_resource($rMessageStream)) - { + if (\is_resource($rMessageStream)) { \fclose($rMessageStream); } $mResult = true; - $sFolderFullName = $this->GetActionParam('MessageFolder', ''); - $iUid = (int) $this->GetActionParam('MessageUid', 0); + $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) - { + if (\strlen($sFolderFullName) && 0 < $iUid) { try { $this->MailClient()->MessageSetFlag($sFolderFullName, new SequenceSet($iUid), MessageFlag::MDNSENT, true, true); @@ -375,32 +356,31 @@ trait Messages throw new ClientException(Notifications::CantSendMessage, $oException); } - if (false === $mResult) - { + if (false === $mResult) { throw new ClientException(Notifications::CantSendMessage); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoMessageSetSeen() : array { - return $this->messageSetFlag(MessageFlag::SEEN, __FUNCTION__); + return $this->messageSetFlag(MessageFlag::SEEN); } public function DoMessageSetSeenToAll() : array { $this->initMailClientConnection(); - $sThreadUids = \trim($this->GetActionParam('ThreadUids', '')); + $sThreadUids = \trim($this->GetActionParam('threadUids', '')); try { $this->MailClient()->MessageSetFlag( - $this->GetActionParam('Folder', ''), + $this->GetActionParam('folder', ''), empty($sThreadUids) ? new SequenceSet('1:*', false) : new SequenceSet(\explode(',', $sThreadUids)), MessageFlag::SEEN, - !empty($this->GetActionParam('SetAction', '0')) + !empty($this->GetActionParam('setAction', '0')) ); } catch (\Throwable $oException) @@ -408,17 +388,17 @@ trait Messages throw new ClientException(Notifications::MailServerError, $oException); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoMessageSetFlagged() : array { - return $this->messageSetFlag(MessageFlag::FLAGGED, __FUNCTION__, true); + return $this->messageSetFlag(MessageFlag::FLAGGED, true); } public function DoMessageSetKeyword() : array { - return $this->messageSetFlag($this->GetActionParam('Keyword', ''), __FUNCTION__, true); + return $this->messageSetFlag($this->GetActionParam('keyword', ''), true); } /** @@ -426,47 +406,36 @@ trait Messages */ public function DoMessage() : array { - $sRawKey = (string) $this->GetActionParam('RawKey', ''); - - $sFolder = ''; - $iUid = 0; - - $aValues = \json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($sRawKey), true); - if ($aValues && 2 <= \count($aValues)) - { + $aValues = $this->decodeRawKey((string) $this->GetActionParam('RawKey', '')); + if ($aValues && 2 <= \count($aValues)) { $sFolder = (string) $aValues[0]; $iUid = (int) $aValues[1]; - - $this->verifyCacheByKey($sRawKey); - } - else - { - $sFolder = $this->GetActionParam('Folder', ''); - $iUid = (int) $this->GetActionParam('Uid', 0); +// $useThreads = !empty($aValues[2]); +// $accountHash = $aValues[3]; + } else { + $sFolder = $this->GetActionParam('folder', ''); + $iUid = (int) $this->GetActionParam('uid', 0); } $oAccount = $this->initMailClientConnection(); try { - $oMessage = $this->MailClient()->Message($sFolder, $iUid, true, - $this->cacherForThreads(), - (int) $this->Config()->Get('imap', 'body_text_limit', 0) - ); + $oMessage = $this->MailClient()->Message($sFolder, $iUid, true, $this->Cacher($oAccount)); } catch (\Throwable $oException) { throw new ClientException(Notifications::CantGetMessage, $oException); } - if ($oMessage) - { + if ($oMessage) { + $ETag = $oMessage->ETag($this->getAccountFromToken()->IncLogin()); + $this->verifyCacheByKey($ETag); $this->Plugins()->RunHook('filter.result-message', array($oMessage)); - - $this->cacheByKey($sRawKey); + $this->cacheByKey($ETag); } - return $this->DefaultResponse(__FUNCTION__, $oMessage); + return $this->DefaultResponse($oMessage); } /** @@ -476,13 +445,12 @@ trait Messages { $this->initMailClientConnection(); - $sFolder = $this->GetActionParam('Folder', ''); - $aUids = \explode(',', (string) $this->GetActionParam('Uids', '')); + $sFolder = $this->GetActionParam('folder', ''); + $aUids = \explode(',', (string) $this->GetActionParam('uids', '')); try { - $this->MailClient()->MessageDelete($sFolder, new SequenceSet($aUids), - !!$this->Config()->Get('imap', 'use_expunge_all_on_delete', false)); + $this->ImapClient()->MessageDelete($sFolder, new SequenceSet($aUids), true); } catch (\Throwable $oException) { @@ -499,7 +467,7 @@ trait Messages \SnappyMail\Log::warning('IMAP', "FolderHash({$sFolder}) Exception: {$oException->getMessage()}"); } - return $this->DefaultResponse(__FUNCTION__, $sHash ? array($sFolder, $sHash) : array($sFromFolder)); + return $this->DefaultResponse($sHash ? array($sFolder, $sHash) : array($sFromFolder)); } /** @@ -509,13 +477,12 @@ trait Messages { $this->initMailClientConnection(); - $sFromFolder = $this->GetActionParam('FromFolder', ''); - $sToFolder = $this->GetActionParam('ToFolder', ''); + $sFromFolder = $this->GetActionParam('fromFolder', ''); + $sToFolder = $this->GetActionParam('toFolder', ''); - $oUids = new SequenceSet(\explode(',', (string) $this->GetActionParam('Uids', ''))); + $oUids = new SequenceSet(\explode(',', (string) $this->GetActionParam('uids', ''))); - if (!empty($this->GetActionParam('MarkAsRead', '0'))) - { + if (!empty($this->GetActionParam('markAsRead', '0'))) { try { $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::SEEN); @@ -526,17 +493,18 @@ trait Messages } } - $sLearning = $this->GetActionParam('Learning', ''); - if ($sLearning) - { + $sLearning = $this->GetActionParam('learning', ''); + if ($sLearning) { try { if ('SPAM' === $sLearning) { +// $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, '\\junk'); $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::JUNK); $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::NOTJUNK, false); } else if ('HAM' === $sLearning) { $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::NOTJUNK); $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, MessageFlag::JUNK, false); +// $this->MailClient()->MessageSetFlag($sFromFolder, $oUids, '\\junk', false); } } catch (\Throwable $oException) @@ -547,10 +515,7 @@ trait Messages try { - $this->MailClient()->MessageMove($sFromFolder, $sToFolder, $oUids, - !!$this->Config()->Get('imap', 'use_move', true), - !!$this->Config()->Get('imap', 'use_expunge_all_on_delete', false) - ); + $this->ImapClient()->MessageMove($sFromFolder, $sToFolder, $oUids); } catch (\Throwable $oException) { @@ -567,7 +532,7 @@ trait Messages \SnappyMail\Log::warning('IMAP', "FolderHash({$sFromFolder}) Exception: {$oException->getMessage()}"); } - return $this->DefaultResponse(__FUNCTION__, $sHash ? array($sFromFolder, $sHash) : array($sFromFolder)); + return $this->DefaultResponse($sHash ? array($sFromFolder, $sHash) : array($sFromFolder)); } /** @@ -579,10 +544,10 @@ trait Messages try { - $this->MailClient()->MessageCopy( - $this->GetActionParam('FromFolder', ''), - $this->GetActionParam('ToFolder', ''), - new SequenceSet(\explode(',', (string) $this->GetActionParam('Uids', ''))) + $this->ImapClient()->MessageCopy( + $this->GetActionParam('fromFolder', ''), + $this->GetActionParam('toFolder', ''), + new SequenceSet(\explode(',', (string) $this->GetActionParam('uids', ''))) ); } catch (\Throwable $oException) @@ -590,7 +555,7 @@ trait Messages throw new ClientException(Notifications::CantCopyMessage, $oException); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoMessageUploadAttachments() : array @@ -602,38 +567,54 @@ trait Messages try { - $aAttachments = $this->GetActionParam('Attachments', array()); - if (\is_array($aAttachments) && \count($aAttachments)) - { - $mResult = array(); - foreach ($aAttachments as $sAttachment) - { - if ($aValues = \RainLoop\Utils::DecodeKeyValuesQ($sAttachment)) - { - $sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : ''; - $iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0; - $sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : ''; + $aAttachments = $this->GetActionParam('attachments', array()); + if (!\is_array($aAttachments)) { + $aAttachments = []; + } + if (\count($aAttachments)) { + $oFilesProvider = $this->FilesProvider(); + foreach ($aAttachments as $mIndex => $sAttachment) { + $aAttachments[$mIndex] = false; + if ($aValues = $this->decodeRawKey($sAttachment)) { + $sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : ''; + $iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0; + $sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : ''; - $sTempName = \md5($sAttachment); - if (!$this->FilesProvider()->FileExists($oAccount, $sTempName)) - { + $sTempName = \sha1($sAttachment); + if (!$oFilesProvider->FileExists($oAccount, $sTempName)) { $this->MailClient()->MessageMimeStream( - function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($oAccount, &$mResult, $sTempName, $sAttachment, $self) { - if (is_resource($rResource)) - { - $sContentType = (empty($sFileName)) ? 'text/plain' : \MailSo\Base\Utils::MimeContentType($sFileName); - $sFileName = $self->MainClearFileName($sFileName, $sContentType, $sMimeIndex); + function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ($oAccount, $sTempName, $self, &$aAttachments, $mIndex) { + if (\is_resource($rResource)) { + $sContentType = + $sContentType + ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName) + ?: \SnappyMail\File\MimeType::fromFilename($sFileName) + ?: 'application/octet-stream'; // 'text/plain' - if ($self->FilesProvider()->PutFile($oAccount, $sTempName, $rResource)) - { - $mResult[$sTempName] = $sAttachment; +// $sFileName = $self->MainClearFileName($sFileName, $sContentType, $sMimeIndex); + $sTempName .= \SnappyMail\File\MimeType::toExtension($sContentType); + + if ($self->FilesProvider()->PutFile($oAccount, $sTempName, $rResource)) { + $aAttachments[$mIndex] = [ +// 'name' => $sFileName, + 'tempName' => $sTempName, + 'mimeType' => $sContentType +// 'size' => 0 + ]; } } }, $sFolder, $iUid, $sMimeIndex); - } - else - { - $mResult[$sTempName] = $sAttachment; + } else { + $rResource = $oFilesProvider->GetFile($oAccount, $sTempName); + $sContentType = \SnappyMail\File\MimeType::fromStream($rResource, $sTempName) + ?: \SnappyMail\File\MimeType::fromFilename($sTempName) + ?: 'application/octet-stream'; // 'text/plain' + $aAttachments[$mIndex] = [ +// 'name' => $sFileName, + 'tempName' => $sTempName, + 'mimeType' => $sContentType +// 'size' => $oFilesProvider->FileSize($oAccount, $sTempName) + ]; } } } @@ -644,7 +625,7 @@ trait Messages throw new ClientException(Notifications::MailServerError, $oException); } - return $this->DefaultResponse(__FUNCTION__, $mResult); + return $this->DefaultResponse($aAttachments); } /** @@ -652,23 +633,23 @@ trait Messages */ public function DoMessagePgpVerify() : array { - $sFolderName = $this->GetActionParam('Folder', ''); - $iUid = (int) $this->GetActionParam('Uid', 0); - $sBodyPart = $this->GetActionParam('BodyPart', ''); - $sSigPart = $this->GetActionParam('SigPart', ''); + $sFolderName = $this->GetActionParam('folder', ''); + $iUid = (int) $this->GetActionParam('uid', 0); + $sBodyPart = $this->GetActionParam('bodyPart', ''); + $sSigPart = $this->GetActionParam('sigPart', ''); if ($sBodyPart) { $result = [ 'text' => \preg_replace('/\\r?\\n/su', "\r\n", $sBodyPart), - 'signature' => $this->GetActionParam('SigPart', '') + 'signature' => $this->GetActionParam('sigPart', '') ]; } else { - $sBodyPartId = $this->GetActionParam('BodyPartId', ''); - $sSigPartId = $this->GetActionParam('SigPartId', ''); -// $sMicAlg = $this->GetActionParam('MicAlg', ''); + $sBodyPartId = $this->GetActionParam('bodyPartId', ''); + $sSigPartId = $this->GetActionParam('sigPartId', ''); +// $sMicAlg = $this->GetActionParam('micAlg', ''); $oAccount = $this->initMailClientConnection(); - $oImapClient = $this->MailClient()->ImapClient(); + $oImapClient = $this->ImapClient(); $oImapClient->FolderExamine($sFolderName); $aParts = [ @@ -699,7 +680,7 @@ trait Messages 'text' => $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.']'), 'signature' => '' ]; - $decode = (new \MailSo\Mime\HeaderCollection($sBodyMime))->ValueByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING); + $decode = (new \MailSo\Mime\HeaderCollection($sBodyMime))->ValueByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING); if ('base64' === $decode) { $result['text'] = \base64_decode($result['text']); } else if ('quoted-printable' === $decode) { @@ -708,8 +689,8 @@ trait Messages } } - // Try by default as OpenPGP.js sets GnuPG to 0 - if ($this->GetActionParam('GnuPG', 1)) { + // Try by default as OpenPGP.js sets useGnuPG to 0 + if ($this->GetActionParam('tryGnuPG', 1)) { $GPG = $this->GnuPG(); if ($GPG) { $info = $this->GnuPG()->verify($result['text'], $result['signature']); @@ -760,7 +741,7 @@ trait Messages } } - return $this->DefaultResponse(__FUNCTION__, $result); + return $this->DefaultResponse($result); } /** @@ -768,156 +749,134 @@ trait Messages * @throws \MailSo\Net\Exceptions\ConnectionException */ private function smtpSendMessage(Account $oAccount, \MailSo\Mime\Message $oMessage, - /*resource*/ &$rMessageStream, int &$iMessageStreamSize, bool $bDsn = false, bool $bAddHiddenRcpt = true) + /*resource*/ &$rMessageStream, int &$iMessageStreamSize, bool $bAddHiddenRcpt = true, + bool $bDsn = false, bool $bRequireTLS = false) { $oRcpt = $oMessage->GetRcpt(); - if ($oRcpt && 0 < $oRcpt->Count()) + if (!$oRcpt || !$oRcpt->count()) { + throw new ClientException(Notifications::InvalidRecipients); + } + + $this->Plugins()->RunHook('filter.smtp-message-stream', + array($oAccount, &$rMessageStream, &$iMessageStreamSize)); + + $this->Plugins()->RunHook('filter.message-rcpt', array($oAccount, $oRcpt)); + + $oSmtpClient = null; + try { - $this->Plugins()->RunHook('filter.smtp-message-stream', - array($oAccount, &$rMessageStream, &$iMessageStreamSize)); + $oFrom = $oMessage->GetFrom(); + $sFrom = $oFrom instanceof \MailSo\Mime\Email ? $oFrom->GetEmail() : ''; + $sFrom = empty($sFrom) ? $oAccount->Email() : $sFrom; - $this->Plugins()->RunHook('filter.message-rcpt', array($oAccount, $oRcpt)); + $this->Plugins()->RunHook('filter.smtp-from', array($oAccount, $oMessage, &$sFrom)); - try + $aHiddenRcpt = array(); + if ($bAddHiddenRcpt) { - $oFrom = $oMessage->GetFrom(); - $sFrom = $oFrom instanceof \MailSo\Mime\Email ? $oFrom->GetEmail() : ''; - $sFrom = empty($sFrom) ? $oAccount->Email() : $sFrom; + $this->Plugins()->RunHook('filter.smtp-hidden-rcpt', array($oAccount, $oMessage, &$aHiddenRcpt)); + } - $this->Plugins()->RunHook('filter.smtp-from', array($oAccount, $oMessage, &$sFrom)); + $oSmtpClient = new \MailSo\Smtp\SmtpClient(); + $oSmtpClient->SetLogger($this->Logger()); - $aHiddenRcpt = array(); - if ($bAddHiddenRcpt) - { - $this->Plugins()->RunHook('filter.smtp-hidden-rcpt', array($oAccount, $oMessage, &$aHiddenRcpt)); - } + $oAccount->SmtpConnectAndLogin($this->Plugins(), $oSmtpClient); - $oSmtpClient = new \MailSo\Smtp\SmtpClient(); - $oSmtpClient->SetLogger($this->Logger()); - $oSmtpClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'smtp_timeout', 60)); + if ($oSmtpClient->Settings->usePhpMail) { + if (\MailSo\Base\Utils::FunctionCallable('mail')) { + $aToCollection = $oMessage->GetTo(); + if ($aToCollection && $oFrom) { + $sRawBody = \stream_get_contents($rMessageStream); + 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); - $bUsePhpMail = false; - $oAccount->SmtpConnectAndLoginHelper($this->Plugins(), $oSmtpClient, $this->Config(), $bUsePhpMail); + $sMailHeaders = $sMailBody = ''; + list($sMailHeaders, $sMailBody) = \explode("\r\n\r\n", $sRawBody, 2); + unset($sRawBody); - if ($bUsePhpMail) - { - if (\MailSo\Base\Utils::FunctionCallable('mail')) - { - $aToCollection = $oMessage->GetTo(); - if ($aToCollection && $oFrom) - { - $sRawBody = \stream_get_contents($rMessageStream); - 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); + if ($this->Config()->Get('labs', 'mail_func_clear_headers', true)) { + $sMailHeaders = \MailSo\Base\Utils::RemoveHeaderFromHeaders($sMailHeaders, array( + MimeEnumHeader::TO_, + MimeEnumHeader::SUBJECT + )); + } - $sMailHeaders = $sMailBody = ''; - list($sMailHeaders, $sMailBody) = \explode("\r\n\r\n", $sRawBody, 2); - unset($sRawBody); + $this->Logger()->WriteDump(array( + $sMailTo, $sMailSubject, $sMailBody, $sMailHeaders + ), \LOG_DEBUG); - if ($this->Config()->Get('labs', 'mail_func_clear_headers', true)) - { - $sMailHeaders = \MailSo\Base\Utils::RemoveHeaderFromHeaders($sMailHeaders, array( - \MailSo\Mime\Enumerations\Header::TO_, - \MailSo\Mime\Enumerations\Header::SUBJECT - )); - } + $bR = $this->Config()->Get('labs', 'mail_func_additional_parameters', false) ? + \mail($sMailTo, $sMailSubject, $sMailBody, $sMailHeaders, '-f'.$oFrom->GetEmail()) : + \mail($sMailTo, $sMailSubject, $sMailBody, $sMailHeaders); - $this->Logger()->WriteDump(array( - $sMailTo, $sMailSubject, $sMailBody, $sMailHeaders - ), \LOG_DEBUG); - - $bR = $this->Config()->Get('labs', 'mail_func_additional_parameters', false) ? - \mail($sMailTo, $sMailSubject, $sMailBody, $sMailHeaders, '-f'.$oFrom->GetEmail()) : - \mail($sMailTo, $sMailSubject, $sMailBody, $sMailHeaders); - - if (!$bR) - { - throw new ClientException(Notifications::CantSendMessage); - } + if (!$bR) { + throw new ClientException(Notifications::CantSendMessage); } } } - else - { - throw new ClientException(Notifications::CantSendMessage); - } + } else { + throw new ClientException(Notifications::CantSendMessage); + } + } else if ($oSmtpClient->IsConnected()) { + if ($iMessageStreamSize && $oSmtpClient->maxSize() && $iMessageStreamSize * 1.33 > $oSmtpClient->maxSize()) { + throw new ClientException(Notifications::ClientViewError, 'Message size '. ($iMessageStreamSize * 1.33) . ' bigger then max ' . $oSmtpClient->maxSize()); } - else if ($oSmtpClient->IsConnected()) - { - if (!empty($sFrom)) - { - $oSmtpClient->MailFrom($sFrom, '', $bDsn); - } - foreach ($oRcpt as /* @var $oEmail \MailSo\Mime\Email */ $oEmail) - { - $oSmtpClient->Rcpt($oEmail->GetEmail(), $bDsn); - } + if (!empty($sFrom)) { + $oSmtpClient->MailFrom($sFrom, 0, $bDsn, $bRequireTLS); + } - if ($bAddHiddenRcpt && \is_array($aHiddenRcpt) && \count($aHiddenRcpt)) - { - foreach ($aHiddenRcpt as $sEmail) - { - if (\preg_match('/^[^@\s]+@[^@\s]+$/', $sEmail)) - { - $oSmtpClient->Rcpt($sEmail); - } + foreach ($oRcpt as /* @var $oEmail \MailSo\Mime\Email */ $oEmail) { + $oSmtpClient->Rcpt($oEmail->GetEmail(), $bDsn); + } + + if ($bAddHiddenRcpt && \is_array($aHiddenRcpt) && \count($aHiddenRcpt)) { + foreach ($aHiddenRcpt as $sEmail) { + if (\preg_match('/^[^@\s]+@[^@\s]+$/', $sEmail)) { + $oSmtpClient->Rcpt($sEmail); } } + } - $oSmtpClient->DataWithStream($rMessageStream); + $oSmtpClient->DataWithStream($rMessageStream); - $oSmtpClient->Disconnect(); - } - } - catch (\MailSo\Net\Exceptions\ConnectionException $oException) - { - if ($this->Config()->Get('labs', 'smtp_show_server_errors')) - { - throw new ClientException(Notifications::ClientViewError, $oException); - } - else - { - throw new ClientException(Notifications::ConnectionError, $oException); - } - } - catch (\MailSo\Smtp\Exceptions\LoginException $oException) - { - throw new ClientException(Notifications::AuthError, $oException); - } - catch (\Throwable $oException) - { - if ($this->Config()->Get('labs', 'smtp_show_server_errors')) - { - throw new ClientException(Notifications::ClientViewError, $oException); - } - else - { - throw $oException; - } + $oSmtpClient->Disconnect(); } } - else + catch (\MailSo\Net\Exceptions\ConnectionException $oException) { - throw new ClientException(Notifications::InvalidRecipients); + if ($oSmtpClient && $oSmtpClient->Settings->viewErrors) { + throw new ClientException(Notifications::ClientViewError, $oException); + } + throw new ClientException(Notifications::ConnectionError, $oException); + } + catch (\MailSo\Smtp\Exceptions\LoginException $oException) + { + throw new ClientException(Notifications::AuthError, $oException); + } + catch (\Throwable $oException) + { + if ($oSmtpClient && $oSmtpClient->Settings->viewErrors) { + throw new ClientException(Notifications::ClientViewError, $oException); + } + throw $oException; } } - private function messageSetFlag(string $sMessageFlag, string $sResponseFunction, bool $bSkipUnsupportedFlag = false) : array + private function messageSetFlag(string $sMessageFlag, bool $bSkipUnsupportedFlag = false) : array { $this->initMailClientConnection(); try { $this->MailClient()->MessageSetFlag( - $this->GetActionParam('Folder', ''), - new SequenceSet(\explode(',', (string) $this->GetActionParam('Uids', ''))), + $this->GetActionParam('folder', ''), + new SequenceSet(\explode(',', (string) $this->GetActionParam('uids', ''))), $sMessageFlag, - !empty($this->GetActionParam('SetAction', '0')), + !empty($this->GetActionParam('setAction', '0')), $bSkipUnsupportedFlag ); } @@ -926,54 +885,31 @@ trait Messages throw new ClientException(Notifications::MailServerError, $oException); } - return $this->TrueResponse($sResponseFunction); + return $this->TrueResponse(); } private function deleteMessageAttachments(Account $oAccount) : void { - $aAttachments = $this->GetActionParam('Attachments', null); + $aAttachments = $this->GetActionParam('attachments', null); - if (\is_array($aAttachments)) - { - foreach (\array_keys($aAttachments) as $sTempName) - { - if ($this->FilesProvider()->FileExists($oAccount, $sTempName)) - { + if (\is_array($aAttachments)) { + foreach (\array_keys($aAttachments) as $sTempName) { + if ($this->FilesProvider()->FileExists($oAccount, $sTempName)) { $this->FilesProvider()->Clear($oAccount, $sTempName); } } } } - /** - * @return MailSo\Cache\CacheClient|null - */ - private function cacherForUids() - { - $oAccount = $this->getAccountFromToken(false); - return ($this->Config()->Get('cache', 'enable', true) && - $this->Config()->Get('cache', 'server_uids', false)) ? $this->Cacher($oAccount) : null; - } - - /** - * @return MailSo\Cache\CacheClient|null - */ - private function cacherForThreads() - { - $oAccount = $this->getAccountFromToken(false); - return !!$this->Config()->Get('imap', 'use_thread', false) ? $this->Cacher($oAccount) : null; - } - private function buildReadReceiptMessage(Account $oAccount) : \MailSo\Mime\Message { - $sReadReceipt = $this->GetActionParam('ReadReceipt', ''); + $sReadReceipt = $this->GetActionParam('readReceipt', ''); $sSubject = $this->GetActionParam('subject', ''); - $sText = $this->GetActionParam('Text', ''); + $sText = $this->GetActionParam('plain', ''); $oIdentity = $this->GetIdentityByID($oAccount, '', true); - if (empty($sReadReceipt) || empty($sSubject) || empty($sText) || !$oIdentity) - { + if (empty($sReadReceipt) || empty($sSubject) || empty($sText) || !$oIdentity) { throw new ClientException(Notifications::UnknownError); } @@ -991,11 +927,9 @@ trait Messages $oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : ''); $sReplyTo = $oIdentity->ReplyTo(); - if (!empty($sReplyTo)) - { + if (!empty($sReplyTo)) { $oReplyTo = new \MailSo\Mime\EmailCollection($sReplyTo); - if ($oReplyTo && $oReplyTo->Count()) - { + if ($oReplyTo && $oReplyTo->count()) { $oMessage->SetReplyTo($oReplyTo); } } @@ -1003,16 +937,15 @@ trait Messages $oMessage->SetSubject($sSubject); $oToEmails = new \MailSo\Mime\EmailCollection($sReadReceipt); - if ($oToEmails && $oToEmails->Count()) - { + if ($oToEmails && $oToEmails->count()) { $oMessage->SetTo($oToEmails); } $this->Plugins()->RunHook('filter.read-receipt-message-plain', array($oAccount, $oMessage, &$sText)); $oPart = new MimePart; - $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"'); - $oPart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/plain; charset="utf-8"'); + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); $oPart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sText))), 'convert.quoted-printable-encode' @@ -1034,10 +967,10 @@ trait Messages $oMessage->SetXMailer('SnappyMail/'.APP_VERSION); } - $sFrom = $this->GetActionParam('From', ''); + $sFrom = $this->GetActionParam('from', ''); $oMessage->SetFrom(\MailSo\Mime\Email::Parse($sFrom)); /* - $oFromIdentity = $this->GetIdentityByID($oAccount, $this->GetActionParam('IdentityID', '')); + $oFromIdentity = $this->GetIdentityByID($oAccount, $this->GetActionParam('identityID', '')); if ($oFromIdentity) { $oMessage->SetFrom(new \MailSo\Mime\Email( @@ -1054,67 +987,49 @@ trait Messages $oFrom = $oMessage->GetFrom(); $oMessage->RegenerateMessageId($oFrom ? $oFrom->GetDomain() : ''); - $oReplyTo = new \MailSo\Mime\EmailCollection($this->GetActionParam('ReplyTo', '')); - if ($oReplyTo->count()) { - $oMessage->SetReplyTo($oReplyTo); - } + $oMessage->SetReplyTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('replyTo', ''))); - if (!empty($this->GetActionParam('ReadReceiptRequest', 0))) { + if (!empty($this->GetActionParam('readReceiptRequest', 0))) { // Read Receipts Reference Main Account Email, Not Identities #147 // $oMessage->SetReadReceipt(($oFromIdentity ?: $oAccount)->Email()); $oMessage->SetReadReceipt($oFrom->GetEmail()); } - if (!empty($this->GetActionParam('MarkAsImportant', 0))) { + if (empty($this->GetActionParam('requireTLS', 0))) { + $oMessage->SetCustomHeader('TLS-Required', 'No'); + } + + if (!empty($this->GetActionParam('markAsImportant', 0))) { $oMessage->SetPriority(\MailSo\Mime\Enumerations\MessagePriority::HIGH); } $oMessage->SetSubject($this->GetActionParam('subject', '')); - $oToEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('To', '')); - if ($oToEmails->count()) { - $oMessage->SetTo($oToEmails); - } + $oMessage->SetTo(new \MailSo\Mime\EmailCollection($this->GetActionParam('to', ''))); + $oMessage->SetCc(new \MailSo\Mime\EmailCollection($this->GetActionParam('cc', ''))); + $oMessage->SetBcc(new \MailSo\Mime\EmailCollection($this->GetActionParam('bcc', ''))); - $oCcEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('Cc', '')); - if ($oCcEmails->count()) { - $oMessage->SetCc($oCcEmails); - } - - $oBccEmails = new \MailSo\Mime\EmailCollection($this->GetActionParam('Bcc', '')); - if ($oBccEmails->count()) { - $oMessage->SetBcc($oBccEmails); - } - - $aDraftInfo = $this->GetActionParam('DraftInfo', null); - if ($bWithDraftInfo && \is_array($aDraftInfo) && !empty($aDraftInfo[0]) && !empty($aDraftInfo[1]) && !empty($aDraftInfo[2])) - { + $aDraftInfo = $this->GetActionParam('draftInfo', null); + if ($bWithDraftInfo && \is_array($aDraftInfo) && !empty($aDraftInfo[0]) && !empty($aDraftInfo[1]) && !empty($aDraftInfo[2])) { $oMessage->SetDraftInfo($aDraftInfo[0], $aDraftInfo[1], $aDraftInfo[2]); } - $sInReplyTo = $this->GetActionParam('InReplyTo', ''); - if (\strlen($sInReplyTo)) { - $oMessage->SetInReplyTo($sInReplyTo); - } - - $sReferences = $this->GetActionParam('References', ''); - if (\strlen($sReferences)) { - $oMessage->SetReferences($sReferences); - } + $oMessage->SetInReplyTo($this->GetActionParam('inReplyTo', '')); + $oMessage->SetReferences($this->GetActionParam('references', '')); $aFoundCids = array(); $aFoundDataURL = array(); $aFoundContentLocationUrls = array(); $oPart; - if ($sSigned = $this->GetActionParam('Signed', '')) { + if ($sSigned = $this->GetActionParam('signed', '')) { $aSigned = \explode("\r\n\r\n", $sSigned, 2); // $sBoundary = \preg_replace('/^.+boundary="([^"]+)".+$/Dsi', '$1', $aSigned[0]); - $sBoundary = $this->GetActionParam('Boundary', ''); + $sBoundary = $this->GetActionParam('boundary', ''); $oPart = new MimePart; $oPart->Headers->AddByName( - \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, + MimeEnumHeader::CONTENT_TYPE, 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"; boundary="'.$sBoundary.'"' ); $oPart->Body = $aSigned[1]; @@ -1124,135 +1039,129 @@ trait Messages unset($oAlternativePart); unset($sSigned); - } else if ($sEncrypted = $this->GetActionParam('Encrypted', '')) { + } else if ($sEncrypted = $this->GetActionParam('encrypted', '')) { $oPart = new MimePart; $oPart->Headers->AddByName( - \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, + MimeEnumHeader::CONTENT_TYPE, 'multipart/encrypted; protocol="application/pgp-encrypted"' ); $oMessage->SubParts->append($oPart); $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $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(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $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); unset($sEncrypted); + } else if ($sHtml = $this->GetActionParam('html', '')) { + $oPart = new MimePart; + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'multipart/alternative'); + $oMessage->SubParts->append($oPart); + + $sHtml = \MailSo\Base\HtmlUtils::BuildHtml($sHtml, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls); + $this->Plugins()->RunHook('filter.message-html', array($oAccount, $oMessage, &$sHtml)); + + // 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); + unset($sPlain); + + // Now add HTML + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/html; 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($sHtml))), + 'convert.quoted-printable-encode' + ); + $oPart->SubParts->append($oAlternativePart); + + unset($oAlternativePart); + unset($sHtml); + } else { - if ($sHtml = $this->GetActionParam('Html', '')) { + $sPlain = $this->GetActionParam('plain', ''); +/* + if ($sSignature = $this->GetActionParam('pgpSignature', null)) { $oPart = new MimePart; $oPart->Headers->AddByName( - \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, - \MailSo\Mime\Enumerations\MimeType::MULTIPART_ALTERNATIVE + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"' ); $oMessage->SubParts->append($oPart); - $sHtml = \MailSo\Base\HtmlUtils::BuildHtml($sHtml, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls); - $this->Plugins()->RunHook('filter.message-html', array($oAccount, $oMessage, &$sHtml)); - - // First add plain - $sPlain = $this->GetActionParam('Text', '') ?: \MailSo\Base\HtmlUtils::ConvertHtmlToPlain($sHtml); - $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sPlain)); $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'text/plain; charset=utf-8'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::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); - unset($sPlain); - - // Now add HTML - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'text/html; charset=utf-8'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'quoted-printable'); - $oAlternativePart->Body = \MailSo\Base\StreamWrappers\Binary::CreateStream( - \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sHtml))), - 'convert.quoted-printable-encode' - ); + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'text/plain; charset="utf-8"; protected-headers="v1"'); + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))); $oPart->SubParts->append($oAlternativePart); - unset($oAlternativePart); - unset($sHtml); + $oAlternativePart = new MimePart; + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); + $oAlternativePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sSignature))); + $oPart->SubParts->append($oAlternativePart); - } else { - $sPlain = $this->GetActionParam('Text', ''); - if ($sSignature = $this->GetActionParam('Signature', null)) { - $oPart = new MimePart; - $oPart->Headers->AddByName( - \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, - 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"' - ); - $oMessage->SubParts->append($oPart); - - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"; protected-headers="v1"'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, 'base64'); - $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sPlain))); - $oPart->SubParts->append($oAlternativePart); - - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); - $oAlternativePart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString(\preg_replace('/\\r?\\n/su', "\r\n", \trim($sSignature))); - $oPart->SubParts->append($oAlternativePart); - } else { - $this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sPlain)); - $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'text/plain; charset="utf-8"'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::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); unset($sSignature); - unset($sPlain); - } + } 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); + unset($sPlain); } unset($oPart); - $aAttachments = $this->GetActionParam('Attachments', null); - if (\is_array($aAttachments)) - { - foreach ($aAttachments as $sTempName => $aData) - { - $sFileName = (string) $aData[0]; - $bIsInline = (bool) $aData[1]; - $sCID = (string) $aData[2]; - $sContentLocation = isset($aData[3]) ? (string) $aData[3] : ''; + $aAttachments = $this->GetActionParam('attachments', null); + if (\is_array($aAttachments)) { + foreach ($aAttachments as $sTempName => $aData) { + $sFileName = (string) $aData['name']; + $bIsInline = (bool) $aData['inline']; + $sCID = (string) $aData['cId']; + $sContentLocation = (string) $aData['location']; + $sMimeType = (string) $aData['type']; $rResource = $this->FilesProvider()->GetFile($oAccount, $sTempName); - if (\is_resource($rResource)) - { + if (\is_resource($rResource)) { $iFileSize = $this->FilesProvider()->FileSize($oAccount, $sTempName); $oMessage->Attachments()->append( new \MailSo\Mime\Attachment($rResource, $sFileName, $iFileSize, $bIsInline, \in_array(trim(trim($sCID), '<>'), $aFoundCids), - $sCID, array(), $sContentLocation + $sCID, array(), $sContentLocation, $sMimeType ) ); } } } - foreach ($aFoundDataURL as $sCidHash => $sDataUrlString) - { + foreach ($aFoundDataURL as $sCidHash => $sDataUrlString) { $aMatch = array(); $sCID = '<'.$sCidHash.'>'; if (\preg_match('/^data:(image\/[a-zA-Z0-9]+);base64,(.+)$/i', $sDataUrlString, $aMatch) && @@ -1260,8 +1169,7 @@ trait Messages { $sRaw = \MailSo\Base\Utils::Base64Decode($aMatch[2]); $iFileSize = \strlen($sRaw); - if (0 < $iFileSize) - { + if (0 < $iFileSize) { $sFileName = \preg_replace('/[^a-z0-9]+/i', '.', $aMatch[1]); $rResource = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sRaw); @@ -1276,8 +1184,8 @@ trait Messages } } - $sFingerprint = $this->GetActionParam('SignFingerprint', ''); - $sPassphrase = $this->GetActionParam('SignPassphrase', ''); + $sFingerprint = $this->GetActionParam('signFingerprint', ''); + $sPassphrase = $this->GetActionParam('signPassphrase', ''); if ($sFingerprint) { $GPG = $this->GnuPG(); $oBody = $oMessage->GetRootPart(); @@ -1302,7 +1210,7 @@ trait Messages $oPart = new MimePart; $oPart->Headers->AddByName( - \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, + MimeEnumHeader::CONTENT_TYPE, 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"' ); $oMessage->SubParts->append($oPart); @@ -1312,13 +1220,13 @@ trait Messages $oPart->SubParts->append($oBody); $oSignaturePart = new MimePart; - $oSignaturePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); - $oSignaturePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pgp-signature; name="signature.asc"'); + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); $oSignaturePart->Body = $sSignature; $oPart->SubParts->append($oSignaturePart); } - $aFingerprints = \json_decode($this->GetActionParam('EncryptFingerprints', ''), true); + $aFingerprints = \json_decode($this->GetActionParam('encryptFingerprints', ''), true); if ($aFingerprints) { $GPG = $this->GnuPG(); $oBody = $oMessage->GetRootPart(); @@ -1337,22 +1245,22 @@ trait Messages $oPart = new MimePart; $oPart->Headers->AddByName( - \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, + MimeEnumHeader::CONTENT_TYPE, 'multipart/encrypted; protocol="application/pgp-encrypted"' ); $oMessage->SubParts->append($oPart); $oAlternativePart = new MimePart; - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $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(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); - $oAlternativePart->Headers->AddByName(\MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); + $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); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php index bc3a026b0..945b81a48 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php @@ -2,6 +2,8 @@ namespace RainLoop\Actions; +use SnappyMail\PGP\Backup; + trait Pgp { /** @@ -74,15 +76,15 @@ trait Pgp { $GPG = $this->GnuPG(); if (!$GPG) { - return $this->FalseResponse(__FUNCTION__); + return $this->FalseResponse(); } $GPG->addDecryptKey( - $this->GetActionParam('KeyId', ''), - $this->GetActionParam('Passphrase', '') + $this->GetActionParam('keyId', ''), + $this->GetActionParam('passphrase', '') ); - $sData = $this->GetActionParam('Data', ''); + $sData = $this->GetActionParam('data', ''); $oPart = null; $result = [ 'data' => '', @@ -102,9 +104,9 @@ trait Pgp // $oPart = \MailSo\Mime\Part::FromStream($rStreamHandle); } }, - $this->GetActionParam('Folder', ''), - (int) $this->GetActionParam('Uid', ''), - $this->GetActionParam('PartId', '') + $this->GetActionParam('folder', ''), + (int) $this->GetActionParam('uid', ''), + $this->GetActionParam('partId', '') ); } @@ -113,21 +115,21 @@ trait Pgp // $result['signatures'] = $oPart->SubParts[0]; } - return $this->DefaultResponse(__FUNCTION__, $result); + return $this->DefaultResponse($result); } public function DoGnupgGetKeys() : array { $GPG = $this->GnuPG(); - return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->keyInfo('') : false); + return $this->DefaultResponse($GPG ? $GPG->keyInfo('') : false); } public function DoGnupgExportKey() : array { $GPG = $this->GnuPG(); - return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->export( - $this->GetActionParam('KeyId', ''), - $this->GetActionParam('Passphrase', '') + return $this->DefaultResponse($GPG ? $GPG->export( + $this->GetActionParam('keyId', ''), + $this->GetActionParam('passphrase', '') ) : false); } @@ -136,29 +138,29 @@ trait Pgp $fingerprint = false; $GPG = $this->GnuPG(); if ($GPG) { - $sName = $this->GetActionParam('Name', ''); - $sEmail = $this->GetActionParam('Email', ''); + $sName = $this->GetActionParam('name', ''); + $sEmail = $this->GetActionParam('email', ''); $fingerprint = $GPG->generateKey( $sName ? "{$sName} <{$sEmail}>" : $sEmail, - $this->GetActionParam('Passphrase', '') + $this->GetActionParam('passphrase', '') ); } - return $this->DefaultResponse(__FUNCTION__, $fingerprint); + return $this->DefaultResponse($fingerprint); } public function DoGnupgDeleteKey() : array { $GPG = $this->GnuPG(); - $sKeyId = $this->GetActionParam('KeyId', ''); + $sKeyId = $this->GetActionParam('keyId', ''); $bPrivate = !!$this->GetActionParam('isPrivate', 0); - return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->deleteKey($sKeyId, $bPrivate) : false); + return $this->DefaultResponse($GPG ? $GPG->deleteKey($sKeyId, $bPrivate) : false); } public function DoGnupgImportKey() : array { - $sKey = $this->GetActionParam('Key', ''); - $sKeyId = $this->GetActionParam('KeyId', ''); - $sEmail = $this->GetActionParam('Email', ''); + $sKey = $this->GetActionParam('key', ''); + $sKeyId = $this->GetActionParam('keyId', ''); + $sEmail = $this->GetActionParam('email', ''); if (!$sKey) { try { @@ -182,7 +184,7 @@ trait Pgp } $GPG = $sKey ? $this->GnuPG() : null; - return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->import($sKey) : false); + return $this->DefaultResponse($GPG ? $GPG->import($sKey) : false); } /** @@ -191,35 +193,7 @@ trait Pgp */ public function DoGetStoredPGPKeys() : array { - $oAccount = $this->getMainAccountFromToken(); - if (!$oAccount) { - return null; - } - - $dir = $this->StorageProvider()->GenerateFilePath( - $oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::PGP - ); - - $keys = []; - foreach (\glob("{$dir}/*") as $file) { - if (\is_file($file)) { - if ('.asc' === \substr($file, -4)) { - $keys[] = \file_get_contents($file); - } else if ('.key' === \substr($file, -4)) { - $key = \json_decode(\file_get_contents($file), true); - $mac = \array_pop($key); - $hash = $oAccount->CryptKey(); - if ($mac === \hash_hmac('sha1', $key[2], $hash)) { - $key[1] = \base64_decode($key[1]); - $key[2] = \base64_decode($key[2]); - $keys[] = \SnappyMail\Crypt::Decrypt($key, $hash); - } - } - } - } - - return $this->DefaultResponse(__FUNCTION__, $keys); + return $this->DefaultResponse(\SnappyMail\PGP\Backup::getKeys()); } /** @@ -232,16 +206,16 @@ trait Pgp $privateKey = $this->GetActionParam('privateKey', ''); $result = [ - 'onServer' => [false, false, false], - 'inGnuPG' => [false, false, false] + 'onServer' => [false, false], + 'inGnuPG' => [false, false] ]; $onServer = (int) $this->GetActionParam('onServer', 0); if ($publicKey && $onServer & 1) { - $result['onServer'][0] = $this->StorePGPKey($publicKey); + $result['onServer'][0] = Backup::PGPKey($publicKey); } if ($privateKey && $onServer & 2) { - $result['onServer'][1] = $this->StorePGPKey($privateKey); + $result['onServer'][1] = Backup::PGPKey($privateKey); } $inGnuPG = (int) $this->GetActionParam('inGnuPG', 0); @@ -256,7 +230,7 @@ trait Pgp } // $revocationCertificate = $this->GetActionParam('revocationCertificate', ''); - return $this->DefaultResponse(__FUNCTION__, $result); + return $this->DefaultResponse($result); } /** @@ -265,34 +239,8 @@ trait Pgp */ public function DoStorePGPKey() : array { - $key = $this->GetActionParam('Key', ''); - $keyId = $this->GetActionParam('KeyId', ''); - return $this->DefaultResponse(__FUNCTION__, ($key && $keyId && $this->StorePGPKey($key, $keyId))); + $key = $this->GetActionParam('key', ''); + $keyId = $this->GetActionParam('keyId', ''); + return $this->DefaultResponse(($key && $keyId && Backup::PGPKey($key, $keyId))); } - - private function StorePGPKey(string $key, string $keyId = '') : bool - { - $oAccount = $this->getMainAccountFromToken(); - if ($oAccount) { - $keyId = $keyId ? "0x{$keyId}" : \sha1($key); - $dir = $this->StorageProvider()->GenerateFilePath( - $oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::PGP, - true - ); - if (\str_contains($key, 'PGP PRIVATE KEY')) { - $hash = $oAccount->CryptKey(); - $key = \SnappyMail\Crypt::Encrypt($key, $hash); - $key[1] = \base64_encode($key[1]); - $key[2] = \base64_encode($key[2]); - $key[] = \hash_hmac('sha1', $key[2], $hash); - return !!\file_put_contents("{$dir}{$keyId}.key", \json_encode($key)); - } - if (\str_contains($key, 'PGP PUBLIC KEY')) { - return !!\file_put_contents("{$dir}{$keyId}_public.asc", $key); - } - } - return false; - } - } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php index d639eea6c..05590dc6b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php @@ -9,22 +9,28 @@ trait Raw */ public function RawViewAsPlain() : bool { - $this->initMailClientConnection(); - - $sRawKey = (string) $this->GetActionParam('RawKey', ''); - $aValues = $this->getDecodedRawKeyValue($sRawKey); - - $sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : ''; - $iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0; - $sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : ''; - - \header('Content-Type: text/plain'); - - return $this->MailClient()->MessageMimeStream(function ($rResource) { - if (\is_resource($rResource)) { - \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); - } - }, $sFolder, $iUid, $sMimeIndex); + $oAccount = $this->getAccountFromToken(); + $sRawKey = $this->GetActionParam('RawKey', ''); + $aValues = $this->decodeRawKey($sRawKey); + if (!empty($aValues['folder']) && !empty($aValues['uid']) + && !empty($aValues['accountHash']) && $aValues['accountHash'] === $oAccount->Hash() + ) { + $this->verifyCacheByKey($sRawKey); + $this->initMailClientConnection(); + \header('Content-Type: text/plain'); + return $this->MailClient()->MessageMimeStream( + function ($rResource) use ($sRawKey) { + if (\is_resource($rResource)) { + $this->cacheByKey($sRawKey); + \MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource); + } + }, + (string) $aValues['folder'], + (int) $aValues['uid'], + isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : '' + ); + } + return false; } public function RawDownload() : bool @@ -49,60 +55,19 @@ trait Raw $oAccount = $this->getAccountFromToken(); - $sData = $this->StorageProvider()->Get($oAccount, + $mData = $this->StorageProvider()->Get($oAccount, \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, 'background' ); - if (!empty($sData)) - { - $aData = \json_decode($sData, true); - unset($sData); - - if (!empty($aData['ContentType']) && !empty($aData['Raw']) && - \in_array($aData['ContentType'], array('image/png', 'image/jpg', 'image/jpeg'))) - { + if (!empty($mData)) { + $mData = \json_decode($mData, true); + if (!empty($mData['ContentType']) && !empty($mData['Raw'])) { $this->cacheByKey($sRawKey); - \header('Content-Type: '.$aData['ContentType']); - echo \base64_decode($aData['Raw']); - unset($aData); - - return true; - } - } - - return false; - } - - public function RawPublic() : bool - { - $sRawKey = (string) $this->GetActionParam('RawKey', ''); - $this->verifyCacheByKey($sRawKey); - - $sHash = $sRawKey; - $sData = ''; - - if (!empty($sHash)) - { - $sData = $this->StorageProvider()->Get(null, - \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, - \RainLoop\KeyPathHelper::PublicFile($sHash) - ); - } - - $aMatch = array(); - if (!empty($sData) && 0 === \strpos($sData, 'data:') && - \preg_match('/^data:([^:]+):/', $sData, $aMatch) && !empty($aMatch[1])) - { - $sContentType = \trim($aMatch[1]); - if (\in_array($sContentType, array('image/png', 'image/jpg', 'image/jpeg'))) - { - $this->cacheByKey($sRawKey); - - \header('Content-Type: '.$sContentType); - echo \preg_replace('/^data:[^:]+:/', '', $sData); - unset($sData); + \header('Content-Type: '.$mData['ContentType']); + echo \base64_decode($mData['Raw']); + unset($mData); return true; } @@ -111,52 +76,51 @@ trait Raw return false; } + /** + * Message, Message Attachment or Zip + */ private function rawSmart(bool $bDownload, bool $bThumbnail = false) : bool { $sRawKey = (string) $this->GetActionParam('RawKey', ''); - $aValues = $this->getDecodedRawKeyValue($sRawKey); - - $sRange = $this->Http()->GetHeader('Range'); + $oAccount = $this->getAccountFromToken(); + $aValues = $this->decodeRawKey($sRawKey); + if (empty($aValues['accountHash']) || $aValues['accountHash'] !== $oAccount->Hash()) { + return false; + } + $sRange = \MailSo\Base\Http::GetHeader('Range'); $aMatch = array(); $sRangeStart = $sRangeEnd = ''; $bIsRangeRequest = false; - - if (!empty($sRange) && 'bytes=0-' !== \strtolower($sRange) && \preg_match('/^bytes=([0-9]+)-([0-9]*)/i', \trim($sRange), $aMatch)) + if (!empty($sRange) && 'bytes=0-' !== \strtolower($sRange) + && \preg_match('/^bytes=([0-9]+)-([0-9]*)/i', \trim($sRange), $aMatch)) { $sRangeStart = $aMatch[1]; $sRangeEnd = $aMatch[2]; - $bIsRangeRequest = true; } - $sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : ''; - $iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0; - $sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : ''; + $sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : ''; + $sContentTypeIn = isset($aValues['mimeType']) ? (string) $aValues['mimeType'] : ''; + $sFileNameIn = isset($aValues['fileName']) ? (string) $aValues['fileName'] : ''; - $sContentTypeIn = isset($aValues['MimeType']) ? (string) $aValues['MimeType'] : ''; - $sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : ''; - $sFileHashIn = isset($aValues['FileHash']) ? (string) $aValues['FileHash'] : ''; - - if (!empty($sFileHashIn)) - { + if (!empty($aValues['fileHash'])) { $this->verifyCacheByKey($sRawKey); - $oAccount = $this->getAccountFromToken(); - // https://github.com/the-djmaze/snappymail/issues/144 if ('.pdf' === \substr($sFileNameIn,-4)) { $sContentTypeOut = 'application/pdf'; // application/octet-stream } else { - $sContentTypeOut = $sContentTypeIn ?: \MailSo\Base\Utils::MimeContentType($sFileNameIn); + $sContentTypeOut = $sContentTypeIn + ?: \SnappyMail\File\MimeType::fromFilename($sFileNameIn) + ?: 'application/octet-stream'; } $sFileNameOut = $this->MainClearFileName($sFileNameIn, $sContentTypeIn, $sMimeIndex); - $rResource = $this->FilesProvider()->GetFile($oAccount, $sFileHashIn); - if (\is_resource($rResource)) - { + $rResource = $this->FilesProvider()->GetFile($oAccount, (string) $aValues['fileHash']); + if (\is_resource($rResource)) { \header('Content-Type: '.$sContentTypeOut); \header('Content-Disposition: attachment; '. \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut))); @@ -170,25 +134,35 @@ trait Raw return false; } - else if (!empty($sFolder) && 0 < $iUid) - { - $this->verifyCacheByKey($sRawKey); + + $sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : ''; + $iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0; + if (empty($sFolder) || 1 > $iUid) { + return false; } - $oAccount = $this->initMailClientConnection(); + $this->verifyCacheByKey($sRawKey); + + $this->initMailClientConnection(); $self = $this; return $this->MailClient()->MessageMimeStream( function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use ( - $self, $oAccount, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail, + $self, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail, $bIsRangeRequest, $sRangeStart, $sRangeEnd ) { - if ($oAccount && \is_resource($rResource)) - { + if (\is_resource($rResource)) { \MailSo\Base\Utils::ResetTimeLimit(); $self->cacheByKey($sRawKey); + $self->logWrite(\print_r([ + $sFileName, + $sContentType, + $sFileNameIn, + $sContentTypeIn + ],true), \LOG_DEBUG, 'RAW'); + if ($sFileNameIn) { $sFileName = $sFileNameIn; } @@ -197,30 +171,28 @@ trait Raw if ('.pdf' === \substr($sFileName, -4)) { // https://github.com/the-djmaze/snappymail/issues/144 $sContentType = 'application/pdf'; - } else if ($sContentTypeIn) { - $sContentType = $sContentTypeIn; - } else if (!$sContentType) { - $sContentType = $sFileName ? \MailSo\Base\Utils::MimeContentType($sFileName) : 'text/plain'; + } else { + $sContentType = $sContentTypeIn + ?: $sContentType +// ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName) + ?: \SnappyMail\File\MimeType::fromFilename($sFileName) + ?: 'application/octet-stream'; } - if (!$bDownload) - { + if (!$bDownload) { $bDetectImageOrientation = $self->Config()->Get('labs', 'image_exif_auto_rotate', false) // Mostly only JPEG has EXIF metadata && 'image/jpeg' == $sContentType; try { - if ($bThumbnail) - { + if ($bThumbnail) { $oImage = static::loadImage($rResource, $bDetectImageOrientation, 48); \header('Content-Disposition: inline; '. \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName.'_thumb60x60.png'))); $oImage->show('png'); // $oImage->show('webp'); // Little Britain: "Safari says NO" exit; - } - else if ($bDetectImageOrientation) - { + } else if ($bDetectImageOrientation) { $oImage = static::loadImage($rResource, $bDetectImageOrientation); \header('Content-Disposition: inline; '. \trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName))); @@ -259,24 +231,20 @@ trait Raw \MailSo\Base\Http::StatusHeader(206); - $iRangeStart = (int) $sRangeStart; - $iRangeEnd = (int) $sRangeEnd; + $iRangeStart = \max(0, \intval($sRangeStart)); + $iRangeEnd = \max(0, \intval($sRangeEnd)); - if ('' === $sRangeEnd) { - $sLoadedData = 0 < $iRangeStart ? \substr($sLoadedData, $iRangeStart) : $sLoadedData; - } else { - if ($iRangeStart < $iRangeEnd) { - $sLoadedData = \substr($sLoadedData, $iRangeStart, $iRangeEnd - $iRangeStart); - } else { - $sLoadedData = 0 < $iRangeStart ? \substr($sLoadedData, $iRangeStart) : $sLoadedData; - } + if ($iRangeEnd && $iRangeStart < $iRangeEnd) { + $sLoadedData = \substr($sLoadedData, $iRangeStart, $iRangeEnd - $iRangeStart); + } else if ($iRangeStart) { + $sLoadedData = \substr($sLoadedData, $iRangeStart); } $iContentLength = \strlen($sLoadedData); if (0 < $iContentLength) { \header('Content-Length: '.$iContentLength); - \header('Content-Range: bytes '.$sRangeStart.'-'.(0 < $iRangeEnd ? $iRangeEnd : $iFullContentLength - 1).'/'.$iFullContentLength); + \header('Content-Range: bytes '.$sRangeStart.'-'.($iRangeEnd ?: $iFullContentLength - 1).'/'.$iFullContentLength); } } else { \header('Content-Length: '.\strlen($sLoadedData)); 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 dcf0a2bc8..821bac496 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 @@ -9,128 +9,64 @@ use RainLoop\Utils; trait Response { /** - * @param mixed $mResult = false + * @param mixed $mResult */ - public function DefaultResponse(string $sActionName, $mResult = false, array $aAdditionalParams = array()) : array + public function DefaultResponse($mResult, array $aAdditionalParams = array(), string $sActionName = '') : array { - $this->Plugins()->RunHook('main.default-response-data', array($sActionName, &$mResult)); - $aResponseItem = $this->mainDefaultResponse($sActionName, $mResult, $aAdditionalParams); - $this->Plugins()->RunHook('main.default-response', array($sActionName, &$aResponseItem)); - return $aResponseItem; - } - - public function TrueResponse(string $sActionName, array $aAdditionalParams = array()) : array - { - $mResult = true; - $this->Plugins()->RunHook('main.default-response-data', array($sActionName, &$mResult)); - $aResponseItem = $this->mainDefaultResponse($sActionName, $mResult, $aAdditionalParams); - $this->Plugins()->RunHook('main.default-response', array($sActionName, &$aResponseItem)); - return $aResponseItem; - } - - public function FalseResponse(string $sActionName, ?int $iErrorCode = null, ?string $sErrorMessage = null, ?string $sAdditionalErrorMessage = null) : array - { - $mResult = false; - $this->Plugins() - ->RunHook('main.default-response-data', array($sActionName, &$mResult)) - ->RunHook('main.default-response-error-data', array($sActionName, &$iErrorCode, &$sErrorMessage)) - ; - - $aAdditionalParams = array(); - if (null !== $iErrorCode) - { - $aAdditionalParams['ErrorCode'] = (int) $iErrorCode; - $aAdditionalParams['ErrorMessage'] = null === $sErrorMessage ? '' : (string) $sErrorMessage; - $aAdditionalParams['ErrorMessageAdditional'] = null === $sAdditionalErrorMessage ? '' : (string) $sAdditionalErrorMessage; + if (false === $mResult) { + if (!isset($aAdditionalParams['ErrorCode'])) { + $aAdditionalParams['ErrorCode'] = 0; + } + if (!isset($aAdditionalParams['ErrorMessage'])) { + $aAdditionalParams['ErrorMessage'] = ''; + } } - $aResponseItem = $this->mainDefaultResponse($sActionName, $mResult, $aAdditionalParams); - - $this->Plugins()->RunHook('main.default-response', array($sActionName, &$aResponseItem)); - return $aResponseItem; + return \array_merge(array( +// 'version' => APP_VERSION, + 'Action' => $sActionName, + 'Result' => $this->responseObject($mResult) + ), $aAdditionalParams); } - public function ExceptionResponse(string $sActionName, \Throwable $oException) : array + public function TrueResponse(array $aAdditionalParams = array()) : array { - $iErrorCode = null; - $sErrorMessage = null; - $sErrorMessageAdditional = null; + return $this->DefaultResponse(true, $aAdditionalParams); + } - if ($oException instanceof \RainLoop\Exceptions\ClientException) - { + public function FalseResponse(int $iErrorCode = 0, string $sErrorMessage = '', string $sAdditionalErrorMessage = '') : array + { + return $this->DefaultResponse(false, [ + 'ErrorCode' => $iErrorCode, + 'ErrorMessage' => $sErrorMessage, + 'ErrorMessageAdditional' => $sAdditionalErrorMessage + ]); + } + + public function ExceptionResponse(\Throwable $oException) : array + { + $iErrorCode = 0; + $sErrorMessage = ''; + $sErrorMessageAdditional = ''; + + if ($oException instanceof \RainLoop\Exceptions\ClientException) { $iErrorCode = $oException->getCode(); - $sErrorMessage = null; - - if ($iErrorCode === Notifications::ClientViewError) - { + if ($iErrorCode === Notifications::ClientViewError) { $sErrorMessage = $oException->getMessage(); } - $sErrorMessageAdditional = $oException->getAdditionalMessage(); - if (empty($sErrorMessageAdditional)) - { - $sErrorMessageAdditional = null; - } - } - else - { + } else { $iErrorCode = Notifications::UnknownError; $sErrorMessage = $oException->getCode().' - '.$oException->getMessage(); } - $oPrevious = $oException->getPrevious(); - if ($oPrevious) - { - $this->Logger()->WriteException($oPrevious); - } - else - { - $this->Logger()->WriteException($oException); - } + $this->logException($oException->getPrevious() ?: $oException); - return $this->FalseResponse($sActionName, $iErrorCode, $sErrorMessage, $sErrorMessageAdditional); - } - - /** - * @param mixed $mResult = false - */ - private function mainDefaultResponse(string $sActionName, $mResult = false, array $aAdditionalParams = array()) : array - { - $sActionName = 'Do' === \substr($sActionName, 0, 2) ? \substr($sActionName, 2) : $sActionName; - $sActionName = \preg_replace('/[^a-zA-Z0-9_]+/', '', $sActionName); - - $aResult = array( -// 'Version' => APP_VERSION, - 'Action' => $sActionName, - 'Result' => $this->responseObject($mResult, $sActionName) - ); - - foreach ($aAdditionalParams as $sKey => $mValue) - { - $aResult[$sKey] = $mValue; - } - - return $aResult; - } - - private function isFileHasThumbnail(string $sFileName) : bool - { - static $aCache = array(); - - $sExt = \MailSo\Base\Utils::GetFileExtension($sFileName); - if (isset($aCache[$sExt])) { - return $aCache[$sExt]; - } - - $bResult = ( - \extension_loaded('gd') - || \extension_loaded('gmagick') - || \extension_loaded('imagick') - ) && \in_array($sExt, ['png', 'gif', 'jpg', 'jpeg']); - - $aCache[$sExt] = $bResult; - - return $bResult; + return $this->DefaultResponse(false, [ + 'ErrorCode' => $iErrorCode, + 'ErrorMessage' => $sErrorMessage, + 'ErrorMessageAdditional' => $sErrorMessageAdditional + ]); } /** @@ -141,124 +77,36 @@ trait Response private $aCheckableFolder = null; private function responseObject($mResponse, string $sParent = '') { - if (!($mResponse instanceof \JsonSerializable)) - { - if (\is_object($mResponse)) - { + if (!($mResponse instanceof \JsonSerializable)) { + if (\is_object($mResponse)) { return '["'.\get_class($mResponse).'"]'; } - if (\is_array($mResponse)) - { - foreach ($mResponse as $iKey => $oItem) - { - $mResponse[$iKey] = $this->responseObject($oItem, $sParent); + if (\is_array($mResponse)) { + foreach ($mResponse as $iKey => $oItem) { + $mResponse[$iKey] = $this->responseObject($oItem, 'Array'); } } return $mResponse; } - if ($mResponse instanceof \MailSo\Mail\Message) - { - $mResult = $mResponse->jsonSerialize(); - - $oAccount = $this->getAccountFromToken(); - - if (!$mResult['DateTimeStampInUTC'] || $this->Config()->Get('labs', 'date_from_headers', false)) { - $iDateTimeStampInUTC = $mResponse->HeaderTimeStampInUTC(); - if ($iDateTimeStampInUTC) { - $mResult['DateTimeStampInUTC'] = $iDateTimeStampInUTC; - } - } - - // \MailSo\Mime\EmailCollection - foreach (['ReplyTo','From','To','Cc','Bcc','Sender','DeliveredTo'] as $prop) { - $mResult[$prop] = $this->responseObject($mResult[$prop], $sParent); - } - - $sSubject = $mResult['subject']; - $mResult['Hash'] = \md5($mResult['Folder'].$mResult['Uid']); - $mResult['RequestHash'] = Utils::EncodeKeyValuesQ(array( - 'Account' => $oAccount->Hash(), - 'Folder' => $mResult['Folder'], - 'Uid' => $mResult['Uid'], - 'MimeType' => 'message/rfc822', - 'FileName' => (\strlen($sSubject) ? \MailSo\Base\Utils::SecureFileName($sSubject) : 'message-'.$mResult['Uid']) . '.eml' - )); - - $mResult['Attachments'] = $this->responseObject($mResponse->Attachments(), $sParent); - - if ('Message' === $sParent) - { - $mResult['DraftInfo'] = $mResponse->DraftInfo(); - $mResult['InReplyTo'] = $mResponse->InReplyTo(); - $mResult['UnsubsribeLinks'] = $mResponse->UnsubsribeLinks(); - $mResult['References'] = $mResponse->References(); - - $mResult['Html'] = $mResponse->Html(); - $mResult['Plain'] = $mResponse->Plain(); - -// $this->GetCapa(Capa::OPEN_PGP) || $this->GetCapa(Capa::GNUPG) - $mResult['PgpSigned'] = $mResponse->PgpSigned(); - $mResult['PgpEncrypted'] = $mResponse->PgpEncrypted(); - - $mResult['ReadReceipt'] = $mResponse->ReadReceipt(); - - if (\strlen($mResult['ReadReceipt']) && !\in_array('$forwarded', $mResult['Flags'])) - { - // \in_array('$mdnsent', $mResult['Flags']) - if (\strlen($mResult['ReadReceipt'])) - { - try - { - $oReadReceipt = \MailSo\Mime\Email::Parse($mResult['ReadReceipt']); - if (!$oReadReceipt) - { - $mResult['ReadReceipt'] = ''; - } - } - catch (\Throwable $oException) { unset($oException); } - } - - if (\strlen($mResult['ReadReceipt']) && '1' === $this->Cacher($oAccount)->Get( - \RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $mResult['Folder'], $mResult['Uid']), '0')) - { - $mResult['ReadReceipt'] = ''; - } - } - } - return $mResult; - } - - if ($mResponse instanceof \MailSo\Mail\Attachment) - { - $mResult = $mResponse->jsonSerialize(); - $mResult['IsThumbnail'] = $this->GetCapa(Capa::ATTACHMENT_THUMBNAILS) && $this->isFileHasThumbnail($mResult['FileName']); - $mResult['Download'] = Utils::EncodeKeyValuesQ(array( - 'Account' => $this->getAccountFromToken()->Hash(), - 'Folder' => $mResult['Folder'], - 'Uid' => $mResult['Uid'], - 'MimeIndex' => $mResult['MimeIndex'], - 'MimeType' => $mResult['MimeType'], - 'FileName' => $mResult['FileName'] - )); - return $mResult; - } - - if ($mResponse instanceof \MailSo\Mail\Folder) - { + if ($mResponse instanceof \MailSo\Mail\Message) { $aResult = $mResponse->jsonSerialize(); - - $aStatus = $mResponse->Status(); - if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UIDNEXT'])) { - $aResult['Hash'] = $this->MailClient()->GenerateFolderHash( - $mResponse->FullName(), - $aStatus['MESSAGES'], - $aStatus['UIDNEXT'], - empty($aStatus['HIGHESTMODSEQ']) ? 0 : $aStatus['HIGHESTMODSEQ'] - ); + 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'])) { + $oAccount = $this->getAccountFromToken(); + if ('1' === $this->Cacher($oAccount)->Get(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $aResult['folder'], $aResult['uid']), '0')) { + $aResult['readReceipt'] = ''; + } + } + return $aResult; + } + + if ($mResponse instanceof \MailSo\Imap\Folder) { + $aResult = $mResponse->jsonSerialize(); if (null === $this->aCheckableFolder) { $aCheckable = \json_decode( @@ -268,27 +116,11 @@ trait Response ); $this->aCheckableFolder = \is_array($aCheckable) ? $aCheckable : array(); } - $aResult['Checkable'] = \in_array($mResponse->FullName(), $this->aCheckableFolder); + $aResult['checkable'] = \in_array($mResponse->FullName, $this->aCheckableFolder); return $aResult; } - if ($mResponse instanceof \MailSo\Base\Collection) - { - $mResult = $mResponse->jsonSerialize(); - $mResult['@Collection'] = $this->responseObject($mResult['@Collection'], $sParent); - if ($mResponse instanceof \MailSo\Mail\EmailCollection) { - return \array_slice($mResult['@Collection'], 0, 100); - } - if ($mResponse instanceof \MailSo\Mail\AttachmentCollection - || $mResponse instanceof \MailSo\Mail\FolderCollection - || $mResponse instanceof \MailSo\Mail\MessageCollection - ) { - return $mResult; - } - return $mResult['@Collection']; - } - return $mResponse; } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php index 45d63cf66..3fa64a8f1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Themes.php @@ -15,7 +15,7 @@ trait Themes && ($oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount))) { $sTheme = (string) $oSettingsLocal->GetConf('Theme', $sTheme); } - $sTheme = $this->ValidateTheme($sTheme) ?: 'Default'; + $sTheme = $this->ValidateTheme($sTheme); } return $sTheme; } @@ -54,16 +54,24 @@ trait Themes } $sDir = APP_INDEX_ROOT_PATH . 'themes'; // custom user themes - if (\is_dir($sDir)) { - $rDirH = \opendir($sDir); - if ($rDirH) { + if (\is_dir($sDir) && ($rDirH = \opendir($sDir))) { + while (($sFile = \readdir($rDirH)) !== false) { + if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile) + && (\file_exists("{$sDir}/{$sFile}/styles.css") || \file_exists("{$sDir}/{$sFile}/styles.less"))) { + $aCache[] = $sFile . '@custom'; + } + } + \closedir($rDirH); + } + + if (\class_exists('OC', false)) { + $sDir = \OC::$SERVERROOT . '/themes'; // custom user themes + if (\is_dir($sDir) && ($rDirH = \opendir($sDir))) { while (($sFile = \readdir($rDirH)) !== false) { - if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile) - && (\file_exists("{$sDir}/{$sFile}/styles.css") || \file_exists("{$sDir}/{$sFile}/styles.less"))) { - $aCache[] = $sFile . '@custom'; + if ('.' !== $sFile[0] && \is_dir("{$sDir}/{$sFile}") && \file_exists("{$sDir}/{$sFile}/snappymail/style.css")) { + $aCache[] = $sFile . '@nextcloud'; } } - \closedir($rDirH); } } @@ -82,43 +90,49 @@ trait Themes return $aCache; } - public function ThemeLink(bool $bAdmin): string - { - return './?/Css/0/' . ($bAdmin ? 'Admin' : 'User') . '/-/' . $this->GetTheme($bAdmin) . '/-/' . $this->StaticCache() . '/Hash/-/'; - } - public function ValidateTheme(string $sTheme): string { - return \in_array($sTheme, $this->GetThemes()) ? $sTheme : $this->Config()->Get('themes', 'default', 'Default'); - } - - public function compileCss(string $sTheme, bool $bAdmin) : string - { - $bCustomTheme = '@custom' === \substr($sTheme, -7); - if ($bCustomTheme) { - $sTheme = \substr($sTheme, 0, -7); - } - - $mResult = array(); - - $sBase = ($bCustomTheme ? \RainLoop\Utils::WebPath() : \RainLoop\Utils::WebVersionPath()) - . "themes/{$sTheme}/"; - - $bLess = false; - - $sThemeCSSFile = ($bCustomTheme ? APP_INDEX_ROOT_PATH : APP_VERSION_ROOT_PATH).'themes/'.$sTheme.'/styles.css'; - if (\is_file($sThemeCSSFile)) { - $mResult[] = \file_get_contents($sThemeCSSFile); - } else { - $sThemeCSSFile = \str_replace('styles.css', 'styles.less', $sThemeCSSFile); - if (\is_file($sThemeCSSFile)) { - $bLess = true; - $mResult[] = "@base: \"{$sBase}\";"; - $mResult[] = \file_get_contents($sThemeCSSFile); + if (!\in_array($sTheme, $this->GetThemes())) { + $sTheme = $this->Config()->Get('webmail', 'theme', 'Default'); + if (!\in_array($sTheme, $this->GetThemes())) { + $sTheme = 'Default'; } } + return $sTheme; + } - $mResult[] = $this->Plugins()->CompileCss($bAdmin, $bLess); + public function compileCss(string $sTheme, bool $bAdmin, bool $bMinified = false) : string + { + $mResult = array(); + $bLess = false; + + if ('@nextcloud' === \substr($sTheme, -10)) { + $sBase = \OC::$WEBROOT . '/'; + $sThemeCSSFile = \OC::$SERVERROOT . '/themes/' . \str_replace('@nextcloud', '/snappymail/style.css', $sTheme); + } else { + $bCustomTheme = '@custom' === \substr($sTheme, -7); + if ($bCustomTheme) { + $sTheme = \substr($sTheme, 0, -7); + $sBase = \RainLoop\Utils::WebPath(); + } else { + $sBase = \RainLoop\Utils::WebVersionPath(); + } + $sBase .= "themes/{$sTheme}/"; + $sThemeCSSFile = ($bCustomTheme ? APP_INDEX_ROOT_PATH : APP_VERSION_ROOT_PATH).'themes/'.$sTheme.'/styles.css'; + if (!\is_file($sThemeCSSFile)) { + $sThemeCSSFile = \str_replace('styles.css', 'styles.less', $sThemeCSSFile); + if (\is_file($sThemeCSSFile)) { + $bLess = true; + $mResult[] = "@base: \"{$sBase}\";"; + $mResult[] = \file_get_contents($sThemeCSSFile); + } + } + } + if (\is_file($sThemeCSSFile)) { + $mResult[] = \file_get_contents($sThemeCSSFile); + } + + $mResult[] = $this->Plugins()->CompileCss($bAdmin, $bLess, $bMinified); $mResult = \preg_replace('@(url\(["\']?)(\\./)?([a-z]+[^:a-z])@', "\$1{$sBase}\$3", @@ -127,4 +141,84 @@ trait Themes return $bLess ? (new \LessPHP\lessc())->compile($mResult) : $mResult; // : \str_replace(';}', '}', \preg_replace('/\\s*([:;{},])\\s*/', '\1', \preg_replace('/\\s+/', ' ', \preg_replace('#/\\*.*?\\*/#s', '', $mResult)))); } + + public function UploadBackground(?array $aFile, int $iError): array + { + $oAccount = $this->getAccountFromToken(); + + if (!$this->GetCapa(\RainLoop\Enumerations\Capa::USER_BACKGROUND)) { + return $this->FalseResponse(); + } + + $sName = ''; + $sHash = ''; + + if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) { + $sMimeType = \SnappyMail\File\MimeType::fromFile($aFile['tmp_name'], $aFile['name']) + ?: \SnappyMail\File\MimeType::fromFilename($aFile['name']) + ?: $aFile['type']; + if (\in_array($sMimeType, array('image/png', 'image/jpg', 'image/jpeg', 'image/webp'))) { + $sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']) + . \SnappyMail\File\MimeType::toExtension($sMimeType); + if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) { + $iError = \RainLoop\Enumerations\UploadError::ON_SAVING; + } else { + $rData = $this->FilesProvider()->GetFile($oAccount, $sSavedName); + if (\is_resource($rData)) { + $sData = \stream_get_contents($rData); + if (!empty($sData) && \strlen($sData)) { + $sName = $aFile['name']; + if (empty($sName)) { + $sName = '_'; + } + + if ($this->StorageProvider()->Put($oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, + 'background', + // Used by RawUserBackground() + \RainLoop\Utils::jsonEncode(array( + 'ContentType' => $sMimeType, + 'Raw' => \base64_encode($sData) + )) + )) { + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $sHash = \MailSo\Base\Utils::Sha1Rand($sName . APP_VERSION . APP_SALT); + + $oSettings->SetConf('UserBackgroundName', $sName); + $oSettings->SetConf('UserBackgroundHash', $sHash); + $this->SettingsProvider()->Save($oAccount, $oSettings); + } + } + } + + unset($sData); + } + + if (\is_resource($rData)) { + \fclose($rData); + } + + unset($rData); + } + + $this->FilesProvider()->Clear($oAccount, $sSavedName); + } else { + $iError = \RainLoop\Enumerations\UploadError::FILE_TYPE; + } + } + + if (UPLOAD_ERR_OK !== $iError) { + $iClientError = 0; + $sError = \RainLoop\Enumerations\UploadError::getUserMessage($iError, $iClientError); + if (!empty($sError)) { + return $this->FalseResponse($iClientError, $sError); + } + } + + return $this->DefaultResponse(!empty($sName) && !empty($sHash) ? array( + 'name' => $sName, + 'hash' => $sHash + ) : 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 dab59f712..26ae9ad7a 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 @@ -15,18 +15,15 @@ trait User use Filters; use Folders; use Messages; + use Attachments; use Pgp; - /** - * @var \RainLoop\Providers\Suggestions - */ - private $oSuggestionsProvider = null; + private ?Suggestions $oSuggestionsProvider = null; public function SuggestionsProvider(): Suggestions { if (null === $this->oSuggestionsProvider) { - $this->oSuggestionsProvider = new Suggestions( - $this->fabrica('suggestions')); + $this->oSuggestionsProvider = new Suggestions($this->fabrica('suggestions')); } return $this->oSuggestionsProvider; @@ -39,177 +36,36 @@ trait User { $sEmail = \MailSo\Base\Utils::Trim($this->GetActionParam('Email', '')); $sPassword = $this->GetActionParam('Password', ''); - $bSignMe = !empty($this->GetActionParam('SignMe', 0)); + $bSignMe = !empty($this->GetActionParam('signMe', 0)); - $this->Logger()->AddSecret($sPassword); + $this->logMask($sPassword); - $oAccount = $this->LoginProcess($sEmail, $sPassword, $bSignMe); - $this->Plugins()->RunHook('login.success', array($oAccount)); + try { + $oAccount = $this->LoginProcess($sEmail, $sPassword, $bSignMe); + } catch (\Throwable $oException) { + $this->loginErrorDelay(); + throw $oException; + } $this->SetAuthToken($oAccount); - $sLanguage = $this->GetActionParam('Language', ''); - if ($oAccount && $sLanguage) - { - $oSettings = $this->SettingsProvider()->Load($oAccount); - if ($oSettings) - { - $sLanguage = $this->ValidateLanguage($sLanguage); - $sCurrentLanguage = $oSettings->GetConf('Language', ''); + $this->Plugins()->RunHook('login.success', array($oAccount)); - if ($sCurrentLanguage !== $sLanguage) - { - $oSettings->SetConf('Language', $sLanguage); + $sLanguage = $this->GetActionParam('language', ''); + if ($oAccount && $sLanguage) { + $oSettings = $this->SettingsProvider()->Load($oAccount); + if ($oSettings) { + $sLanguage = $this->ValidateLanguage($sLanguage); + $sCurrentLanguage = $oSettings->GetConf('language', ''); + + if ($sCurrentLanguage !== $sLanguage) { + $oSettings->SetConf('language', $sLanguage); $this->SettingsProvider()->Save($oAccount, $oSettings); } } } - return $this->DefaultResponse(__FUNCTION__, $this->AppData(false)); - } - - /** - * @throws \MailSo\RuntimeException - */ - public function DoAttachmentsActions() : array - { - if (!$this->GetCapa(Capa::ATTACHMENTS_ACTIONS)) - { - return $this->FalseResponse(__FUNCTION__); - } - - $oAccount = $this->initMailClientConnection(); - - $sAction = $this->GetActionParam('Do', ''); - $aHashes = $this->GetActionParam('Hashes', null); - - $mResult = false; - $bError = false; - $aData = false; - - if (\is_array($aHashes) && \count($aHashes)) - { - $aData = array(); - foreach ($aHashes as $sZipHash) - { - $aResult = $this->getMimeFileByHash($oAccount, $sZipHash); - if (!empty($aResult['FileHash'])) - { - $aData[] = $aResult; - } - else - { - $bError = true; - break; - } - } - } - - $oFilesProvider = $this->FilesProvider(); - if (!empty($sAction) && !$bError && \is_array($aData) && \count($aData) && - $oFilesProvider && $oFilesProvider->IsActive()) - { - $bError = false; - switch (\strtolower($sAction)) - { - case 'zip': - - $sZipHash = \MailSo\Base\Utils::Sha1Rand(); - $sZipFileName = $oFilesProvider->GenerateLocalFullFileName($oAccount, $sZipHash); - - if (!empty($sZipFileName)) { - if (\class_exists('ZipArchive')) { - $oZip = new \ZipArchive(); - $oZip->open($sZipFileName, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE); - $oZip->setArchiveComment('SnappyMail/'.APP_VERSION); - foreach ($aData as $aItem) { - $sFileName = (string) (isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat'); - $sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : ''); - if (!empty($sFileHash)) { - $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $sFileHash); - if (!$oZip->addFile($sFullFileNameHash, $sFileName)) { - $bError = true; - } - } - } - - if (!$bError) { - $bError = !$oZip->close(); - } else { - $oZip->close(); - } -/* - } else { - @\unlink($sZipFileName); - $oZip = new \SnappyMail\Stream\ZIP($sZipFileName); -// $oZip->setArchiveComment('SnappyMail/'.APP_VERSION); - foreach ($aData as $aItem) { - $sFileName = (string) (isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat'); - $sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : ''); - if (!empty($sFileHash)) { - $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $sFileHash); - if (!$oZip->addFile($sFullFileNameHash, $sFileName)) { - $bError = true; - } - } - } - $oZip->close(); -*/ - } else { - @\unlink($sZipFileName); - $oZip = new \PharData($sZipFileName . '.zip', 0, null, \Phar::ZIP); - $oZip->compressFiles(\Phar::GZ); - foreach ($aData as $aItem) { - $sFileName = (isset($aItem['FileName']) ? (string) $aItem['FileName'] : 'file.dat'); - $sFileHash = (isset($aItem['FileHash']) ? (string) $aItem['FileHash'] : ''); - if ($sFileHash) { - $sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $sFileHash); - $oZip->addFile($sFullFileNameHash, $sFileName); - } - } - $oZip->compressFiles(\Phar::GZ); - unset($oZip); - \rename($sZipFileName . '.zip', $sZipFileName); - } - - foreach ($aData as $aItem) { - $sFileHash = (isset($aItem['FileHash']) ? (string) $aItem['FileHash'] : ''); - if ($sFileHash) { - $oFilesProvider->Clear($oAccount, $sFileHash); - } - } - - if (!$bError) { - $mResult = array( - 'FileHash' => Utils::EncodeKeyValuesQ(array( - 'Account' => $oAccount ? $oAccount->Hash() : '', - 'FileName' => 'attachments.zip', - 'MimeType' => 'application/zip', - 'FileHash' => $sZipHash - )) - ); - } - } - break; - - default: - $data = new \SnappyMail\AttachmentsAction; - $data->action = $sAction; - $data->items = $aData; - $data->filesProvider = $oFilesProvider; - $data->account = $oAccount; - $this->Plugins()->RunHook('json.attachments', array($data)); - $mResult = $data->result; - break; - } - } - else - { - $bError = true; - } - - $this->requestSleep(); - return $this->DefaultResponse(__FUNCTION__, $bError ? false : $mResult); + return $this->DefaultResponse($this->AppData(false)); } public function DoLogout() : array @@ -217,7 +73,7 @@ trait User $bMain = true; // empty($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY]); $this->Logout($bMain); $bMain && $this->ClearSignMeData(); - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoAppDelayStart() : array @@ -240,20 +96,17 @@ trait User $iMainCacheTime = !empty($aTimers[0]) && \is_numeric($aTimers[0]) ? (int) $aTimers[0] : 0; $iFilesCacheTime = !empty($aTimers[1]) && \is_numeric($aTimers[1]) ? (int) $aTimers[1] : 0; - if (0 === $iMainCacheTime || $iMainCacheTime + $iOneDay1 < \time()) - { + if (0 === $iMainCacheTime || $iMainCacheTime + $iOneDay1 < \time()) { $bMainCache = true; $iMainCacheTime = \time(); } - if (0 === $iFilesCacheTime || $iFilesCacheTime + $iOneDay2 < \time()) - { + if (0 === $iFilesCacheTime || $iFilesCacheTime + $iOneDay2 < \time()) { $bFilesCache = true; $iFilesCacheTime = \time(); } - if ($bMainCache || $bFilesCache) - { + if ($bMainCache || $bFilesCache) { if (!$this->StorageProvider()->Put(null, \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, 'Cache/Timers', \implode(',', array($iMainCacheTime, $iFilesCacheTime)))) @@ -262,26 +115,23 @@ trait User } } - if ($bMainCache) - { - $this->Logger()->Write('Cacher GC: Begin'); + if ($bMainCache) { + $this->logWrite('Cacher GC: Begin'); $this->Cacher()->GC(48); - $this->Logger()->Write('Cacher GC: End'); + $this->logWrite('Cacher GC: End'); - $this->Logger()->Write('Storage GC: Begin'); + $this->logWrite('Storage GC: Begin'); $this->StorageProvider()->GC(); - $this->Logger()->Write('Storage GC: End'); - } - else if ($bFilesCache) - { - $this->Logger()->Write('Files GC: Begin'); + $this->logWrite('Storage GC: End'); + } else if ($bFilesCache) { + $this->logWrite('Files GC: Begin'); $this->FilesProvider()->GC(48); - $this->Logger()->Write('Files GC: End'); + $this->logWrite('Files GC: End'); } $this->Plugins()->RunHook('service.app-delay-start-end'); - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoSettingsUpdate() : array @@ -294,28 +144,23 @@ trait User $oSettings = $this->SettingsProvider()->Load($oAccount); $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); - if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) - { - $this->setSettingsFromParams($oSettings, 'Language', 'string', function ($sLanguage) use ($self) { + if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) { + $this->setSettingsFromParams($oSettings, 'language', 'string', function ($sLanguage) use ($self) { return $self->ValidateLanguage($sLanguage); }); + } else { +// $oSettings->SetConf('language', $this->ValidateLanguage($oConfig->Get('webmail', 'language', 'en'))); } - else - { -// $oSettings->SetConf('Language', $this->ValidateLanguage($oConfig->Get('webmail', 'language', 'en'))); - } + $this->setSettingsFromParams($oSettings, 'hourCycle', 'string'); - if ($this->GetCapa(Capa::THEMES)) - { + if ($this->GetCapa(Capa::THEMES)) { $this->setSettingsFromParams($oSettingsLocal, 'Theme', 'string', function ($sTheme) use ($self) { return $self->ValidateTheme($sTheme); }); $this->setSettingsFromParams($oSettings, 'fontSansSerif', 'string'); $this->setSettingsFromParams($oSettings, 'fontSerif', 'string'); $this->setSettingsFromParams($oSettings, 'fontMono', 'string'); - } - else - { + } else { // $oSettingsLocal->SetConf('Theme', $this->ValidateTheme($oConfig->Get('webmail', 'theme', 'Default'))); } @@ -332,13 +177,21 @@ trait User $this->setSettingsFromParams($oSettings, 'EditorDefaultType', 'string'); $this->setSettingsFromParams($oSettings, 'requestReadReceipt', 'bool'); $this->setSettingsFromParams($oSettings, 'requestDsn', 'bool'); + $this->setSettingsFromParams($oSettings, 'requireTLS', 'bool'); $this->setSettingsFromParams($oSettings, 'pgpSign', 'bool'); $this->setSettingsFromParams($oSettings, 'pgpEncrypt', 'bool'); + $this->setSettingsFromParams($oSettings, 'allowSpellcheck', 'bool'); $this->setSettingsFromParams($oSettings, 'ViewHTML', 'bool'); - $this->setSettingsFromParams($oSettings, 'ShowImages', 'bool'); + $this->setSettingsFromParams($oSettings, 'ViewImages', 'string'); + $this->setSettingsFromParams($oSettings, 'ViewImagesWhitelist', 'string'); $this->setSettingsFromParams($oSettings, 'RemoveColors', 'bool'); + $this->setSettingsFromParams($oSettings, 'AllowStyles', 'bool'); $this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool'); + $this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool'); + $this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int'); + $this->setSettingsFromParams($oSettings, 'simpleAttachmentsList', 'bool'); + $this->setSettingsFromParams($oSettings, 'listGrouped', 'bool'); $this->setSettingsFromParams($oSettings, 'ContactsAutosave', 'bool'); $this->setSettingsFromParams($oSettings, 'DesktopNotifications', 'bool'); $this->setSettingsFromParams($oSettings, 'SoundNotification', 'bool'); @@ -348,6 +201,7 @@ trait User $this->setSettingsFromParams($oSettings, 'AutoLogout', 'int'); $this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int'); $this->setSettingsFromParams($oSettings, 'MsgDefaultAction', 'int'); + $this->setSettingsFromParams($oSettings, 'showNextMessage', 'bool'); $this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int'); $this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int'); @@ -358,9 +212,10 @@ trait User $this->setSettingsFromParams($oSettingsLocal, 'HideUnsubscribed', 'bool'); $this->setSettingsFromParams($oSettingsLocal, 'HideDeleted', 'bool'); $this->setSettingsFromParams($oSettingsLocal, 'UnhideKolabFolders', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'ShowUnreadCount', 'bool'); + $this->setSettingsFromParams($oSettingsLocal, 'CheckMailInterval', 'int'); - return $this->DefaultResponse(__FUNCTION__, - $this->SettingsProvider()->Save($oAccount, $oSettings) && + return $this->DefaultResponse($this->SettingsProvider()->Save($oAccount, $oSettings) && $this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal)); } @@ -368,21 +223,20 @@ trait User { $oAccount = $this->initMailClientConnection(); - if (!$this->GetCapa(Capa::QUOTA)) - { - return $this->DefaultResponse(__FUNCTION__, array(0, 0, 0, 0)); + if (!$this->GetCapa(Capa::QUOTA)) { + return $this->DefaultResponse(array(0, 0, 0, 0)); } try { - $aQuota = $this->MailClient()->QuotaRoot(); + $aQuota = $this->ImapClient()->QuotaRoot(); } catch (\Throwable $oException) { throw new ClientException(Notifications::MailServerError, $oException); } - return $this->DefaultResponse(__FUNCTION__, $aQuota); + return $this->DefaultResponse($aQuota); } public function DoSuggestions() : array @@ -392,86 +246,27 @@ trait User $sQuery = \trim($this->GetActionParam('Query', '')); $iLimit = (int) $this->Config()->Get('contacts', 'suggestions_limit', 20); - $aResult = array(); - $this->Plugins()->RunHook('json.suggestions-input-parameters', array(&$sQuery, &$iLimit, $oAccount)); - $iLimit = (int) $iLimit; - if (5 > $iLimit) - { - $iLimit = 5; + $aResult = array(); + + if ($oSuggestionsProvider = $this->SuggestionsProvider()) { + $aResult = $oSuggestionsProvider->Process($oAccount, $sQuery, $iLimit); } - $this->Plugins()->RunHook('json.suggestions-pre', array(&$aResult, $sQuery, $oAccount, $iLimit)); - - if ($iLimit > \count($aResult) && \strlen($sQuery)) - { - try - { - // Address Book - $oAddressBookProvider = $this->AddressBookProvider($oAccount); - if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) - { - $aSuggestions = $oAddressBookProvider->GetSuggestions($sQuery, $iLimit); - if (!\count($aResult)) - { - $aResult = $aSuggestions; - } - else - { - $aResult = \array_merge($aResult, $aSuggestions); - } - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException); - } - } - - if ($iLimit > \count($aResult) && \strlen($sQuery)) - { - $oSuggestionsProvider = $this->SuggestionsProvider(); - if ($oSuggestionsProvider && $oSuggestionsProvider->IsActive()) - { - $aSuggestionsProviderResult = $oSuggestionsProvider->Process($oAccount, $sQuery, $iLimit); - if (\is_array($aSuggestionsProviderResult) && \count($aSuggestionsProviderResult)) - { - $aResult = \array_merge($aResult, $aSuggestionsProviderResult); - } - } - - } - - $aResult = Utils::RemoveSuggestionDuplicates($aResult); - if ($iLimit < \count($aResult)) - { - $aResult = \array_slice($aResult, 0, $iLimit); - } - - $this->Plugins()->RunHook('json.suggestions-post', array(&$aResult, $sQuery, $oAccount, $iLimit)); - - $aResult = Utils::RemoveSuggestionDuplicates($aResult); - if ($iLimit < \count($aResult)) - { - $aResult = \array_slice($aResult, 0, $iLimit); - } - - return $this->DefaultResponse(__FUNCTION__, $aResult); + return $this->DefaultResponse($aResult); } public function DoClearUserBackground() : array { $oAccount = $this->getAccountFromToken(); - if (!$this->GetCapa(Capa::USER_BACKGROUND)) - { - return $this->FalseResponse(__FUNCTION__); + if (!$this->GetCapa(Capa::USER_BACKGROUND)) { + return $this->FalseResponse(); } $oSettings = $this->SettingsProvider()->Load($oAccount); - if ($oAccount && $oSettings) - { + if ($oAccount && $oSettings) { $this->StorageProvider()->Clear($oAccount, \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, 'background' @@ -481,61 +276,13 @@ trait User $oSettings->SetConf('UserBackgroundHash', ''); } - return $this->DefaultResponse(__FUNCTION__, $oAccount && $oSettings ? + return $this->DefaultResponse($oAccount && $oSettings ? $this->SettingsProvider()->Save($oAccount, $oSettings) : false); } - private function getMimeFileByHash(\RainLoop\Model\Account $oAccount, string $sHash) : array - { - $aValues = $this->getDecodedRawKeyValue($sHash); - - $sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : ''; - $iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0; - $sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : ''; - - $sContentTypeIn = isset($aValues['MimeType']) ? (string) $aValues['MimeType'] : ''; - $sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : ''; - - $oFileProvider = $this->FilesProvider(); - - $sResultHash = ''; - - $mResult = $this->MailClient()->MessageMimeStream(function ($rResource, $sContentType, $sFileName, $sMimeIndex = '') - use ($oAccount, $oFileProvider, $sFileNameIn, $sContentTypeIn, &$sResultHash) { - - unset($sContentType, $sFileName, $sMimeIndex); - - if ($oAccount && \is_resource($rResource)) - { - $sHash = \MailSo\Base\Utils::Sha1Rand($sFileNameIn.'~'.$sContentTypeIn); - $rTempResource = $oFileProvider->GetFile($oAccount, $sHash, 'wb+'); - - if (\is_resource($rTempResource)) - { - if (false !== \MailSo\Base\Utils::MultipleStreamWriter($rResource, array($rTempResource))) - { - $sResultHash = $sHash; - } - - \fclose($rTempResource); - } - } - - }, $sFolder, $iUid, $sMimeIndex); - - $aValues['FileHash'] = ''; - if ($mResult) - { - $aValues['FileHash'] = $sResultHash; - } - - return $aValues; - } - private function setSettingsFromParams(\RainLoop\Settings $oSettings, string $sConfigName, string $sType = 'string', ?callable $cCallback = null) : void { - if ($this->HasActionParam($sConfigName)) - { + if ($this->HasActionParam($sConfigName)) { $sValue = $this->GetActionParam($sConfigName, ''); switch ($sType) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php index eee191601..33f7fa2cf 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -9,11 +9,12 @@ use RainLoop\Model\MainAccount; use RainLoop\Model\AdditionalAccount; use RainLoop\Providers\Storage\Enumerations\StorageType; use RainLoop\Exceptions\ClientException; +use SnappyMail\Cookies; trait UserAuth { /** - * @var string + * @var bool | null | Account */ private $oAdditionalAuthAccount = false; private $oMainAuthAccount = false; @@ -21,10 +22,8 @@ trait UserAuth /** * @throws \RainLoop\Exceptions\ClientException */ - public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false, bool $bMainAccount = true): Account + public function resolveLoginCredentials(string &$sEmail, string &$sPassword, string &$sLogin): void { - $sInputEmail = $sEmail; - $this->Plugins()->RunHook('login.credentials.step-1', array(&$sEmail)); $sEmail = \MailSo\Base\Utils::Trim($sEmail); @@ -33,13 +32,13 @@ trait UserAuth } if (!\str_contains($sEmail, '@')) { - $this->Logger()->Write('The email address "' . $sEmail . '" is not complete', \LOG_INFO, 'LOGIN'); + $this->logWrite('The email address "' . $sEmail . '" is not complete', \LOG_INFO, 'LOGIN'); $bAdded = false; if ($this->Config()->Get('login', 'determine_user_domain', false)) { - $sUserHost = \trim($this->Http()->GetHost(false, true, true)); - $this->Logger()->Write('Determined user domain: ' . $sUserHost, \LOG_INFO, 'LOGIN'); + $sUserHost = \trim($this->Http()->GetHost(true, true)); + $this->logWrite('Determined user domain: ' . $sUserHost, \LOG_INFO, 'LOGIN'); $aDomainParts = \explode('.', $sUserHost); $iLimit = \min(\count($aDomainParts), 14); @@ -51,13 +50,13 @@ trait UserAuth $oDomain = $oDomainProvider->Load($sLine, false); if ($oDomain) { $bAdded = true; - $this->Logger()->Write('Check "' . $sLine . '": OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')', + $this->logWrite('Check "' . $sLine . '": OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')', \LOG_INFO, 'LOGIN'); $sEmail .= '@' . $sLine; break; } else { - $this->Logger()->Write('Check "' . $sLine . '": NO', \LOG_INFO, 'LOGIN'); + $this->logWrite('Check "' . $sLine . '": NO', \LOG_INFO, 'LOGIN'); } \array_shift($aDomainParts); @@ -67,17 +66,17 @@ trait UserAuth $oDomain = $oDomainProvider->Load($sUserHost, true); if ($oDomain) { $bAdded = true; - $this->Logger()->Write('Check "' . $sUserHost . '" with wildcard: OK (' . $sEmail . ' > ' . $sEmail . '@' . $sUserHost . ')', + $this->logWrite('Check "' . $sUserHost . '" with wildcard: OK (' . $sEmail . ' > ' . $sEmail . '@' . $sUserHost . ')', \LOG_INFO, 'LOGIN'); $sEmail .= '@' . $sUserHost; } else { - $this->Logger()->Write('Check "' . $sUserHost . '" with wildcard: NO', \LOG_INFO, 'LOGIN'); + $this->logWrite('Check "' . $sUserHost . '" with wildcard: NO', \LOG_INFO, 'LOGIN'); } } if (!$bAdded) { - $this->Logger()->Write('Domain was not found!', \LOG_INFO, 'LOGIN'); + $this->logWrite('Domain was not found!', \LOG_INFO, 'LOGIN'); } } @@ -89,34 +88,41 @@ trait UserAuth } else if ('gethostname' === $sDefDomain) { $sDefDomain = \gethostname(); } - $this->Logger()->Write('Default domain "' . $sDefDomain . '" was used. (' . $sEmail . ' > ' . $sEmail . '@' . $sDefDomain . ')', + $this->logWrite('Default domain "' . $sDefDomain . '" was used. (' . $sEmail . ' > ' . $sEmail . '@' . $sDefDomain . ')', \LOG_INFO, 'LOGIN'); $sEmail .= '@' . $sDefDomain; } else { - $this->Logger()->Write('Default domain not configured.', \LOG_INFO, 'LOGIN'); + $this->logWrite('Default domain not configured.', \LOG_INFO, 'LOGIN'); } } } $this->Plugins()->RunHook('login.credentials.step-2', array(&$sEmail, &$sPassword)); - if (!\str_contains($sEmail, '@') || !\strlen($sPassword)) { - $this->loginErrorDelay(); - - throw new ClientException(Notifications::InvalidInputArgument); - } - - $this->Logger()->AddSecret($sPassword); - $sLogin = $sEmail; if ($this->Config()->Get('login', 'login_lowercase', true)) { $sLogin = \mb_strtolower($sLogin); } + $this->logMask($sPassword); $this->Plugins()->RunHook('login.credentials', array(&$sEmail, &$sLogin, &$sPassword)); + $this->logMask($sPassword); + } - $this->Logger()->AddSecret($sPassword); + /** + * @throws \RainLoop\Exceptions\ClientException + */ + public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false, bool $bMainAccount = true): Account + { + $sInputEmail = $sEmail; + + $sLogin = ''; + $this->resolveLoginCredentials($sEmail, $sPassword, $sLogin); + + if (!\str_contains($sEmail, '@') || !\strlen($sPassword)) { + throw new ClientException(Notifications::InvalidInputArgument); + } $oAccount = null; try { @@ -128,20 +134,13 @@ trait UserAuth } } catch (\Throwable $oException) { $this->LoggerAuthHelper($oAccount, $this->getAdditionalLogParamsByUserLogin($sInputEmail)); - $this->loginErrorDelay(); throw $oException; } - try { - $this->CheckMailConnection($oAccount, true); - if ($bMainAccount) { - $bSignMe && $this->SetSignMeToken($oAccount); - $this->StorageProvider()->Put($oAccount, StorageType::SESSION, Utils::GetSessionToken(), 'true'); - } - } catch (\Throwable $oException) { - $this->loginErrorDelay(); - - throw $oException; + $this->imapConnect($oAccount, true); + if ($bMainAccount) { + $bSignMe && $this->SetSignMeToken($oAccount); + $this->StorageProvider()->Put($oAccount, StorageType::SESSION, Utils::GetSessionToken(), 'true'); } return $oAccount; @@ -150,12 +149,12 @@ trait UserAuth private static function SetAccountCookie(string $sName, ?Account $oAccount) { if ($oAccount) { - Utils::SetCookie( + Cookies::set( $sName, \MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($oAccount)) ); } else { - Utils::ClearCookie($sName); + Cookies::clear($sName); } } @@ -182,7 +181,8 @@ trait UserAuth } // Test the login - $this->CheckMailConnection($oAccount); + $oImapClient = new \MailSo\Imap\ImapClient; + $this->imapConnect($oAccount, false, $oImapClient); $this->SetAdditionalAuthToken($oAccount); return true; @@ -192,7 +192,7 @@ trait UserAuth /** * Returns RainLoop\Model\AdditionalAccount when it exists, - * else returns RainLoop\Model\Account when it exists, + * else returns RainLoop\Model\MainAccount when it exists, * else null * * @throws \RainLoop\Exceptions\ClientException @@ -202,7 +202,7 @@ trait UserAuth $this->getMainAccountFromToken($bThrowExceptionOnFalse); if (false === $this->oAdditionalAuthAccount && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) { - $aData = Utils::GetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); + $aData = Cookies::getSecure(self::AUTH_ADDITIONAL_TOKEN_KEY); if ($aData) { $this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray( $this, @@ -212,7 +212,7 @@ trait UserAuth } if (!$this->oAdditionalAuthAccount) { $this->oAdditionalAuthAccount = null; - Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); + Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); } } @@ -226,46 +226,41 @@ trait UserAuth { if (false === $this->oMainAuthAccount) try { $this->oMainAuthAccount = null; - if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) { - Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY); - Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); -// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY); -// Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); - Utils::ClearCookie(Utils::SESSION_TOKEN); - } - $aData = Utils::GetSecureCookie(self::AUTH_SPEC_TOKEN_KEY); + $aData = Cookies::getSecure(self::AUTH_SPEC_TOKEN_KEY); if ($aData) { /** * Server side control/kickout of logged in sessions * https://github.com/the-djmaze/snappymail/issues/151 */ - if (empty($_COOKIE[Utils::SESSION_TOKEN])) { + $sToken = Utils::GetSessionToken(false); + if (!$sToken) { // \MailSo\Base\Http::StatusHeader(401); - $this->Logout(true); -// $sAdditionalMessage = $this->StaticI18N('SESSION_UNDEFINED'); - \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN empty'); - throw new ClientException(Notifications::InvalidToken, null, 'Session undefined'); - } - $oMainAuthAccount = MainAccount::NewInstanceFromTokenArray( - $this, - $aData, - $bThrowExceptionOnFalse - ); - $oMainAuthAccount || \SnappyMail\Log::notice('TOKENS', 'AUTH_SPEC_TOKEN_KEY invalid'); - $sToken = $oMainAuthAccount ? Utils::GetSessionToken(false) : null; - $sTokenValue = $sToken ? $this->StorageProvider()->Get($oMainAuthAccount, StorageType::SESSION, $sToken) : null; - if ($oMainAuthAccount && $sTokenValue) { - $this->oMainAuthAccount = $oMainAuthAccount; - } else { - if ($oMainAuthAccount) { - $sToken || \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN not found'); - if ($sToken) { - $oMainAuthAccount && $this->StorageProvider()->Clear($oMainAuthAccount, StorageType::SESSION, $sToken); - $sTokenValue || \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN value invalid: ' . \gettype($sTokenValue)); - } + if (isset($_COOKIE[Utils::SESSION_TOKEN])) { + \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN invalid'); + } else { + \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN not set'); } - Utils::ClearCookie(Utils::SESSION_TOKEN); + } else { + $oMainAuthAccount = MainAccount::NewInstanceFromTokenArray( + $this, + $aData, + $bThrowExceptionOnFalse + ); + if ($oMainAuthAccount) { + $sTokenValue = $this->StorageProvider()->Get($oMainAuthAccount, StorageType::SESSION, $sToken); + if ($sTokenValue) { + $this->oMainAuthAccount = $oMainAuthAccount; + } else { + $this->StorageProvider()->Clear($oMainAuthAccount, StorageType::SESSION, $sToken); + \SnappyMail\Log::notice('TOKENS', 'SESSION_TOKEN value invalid: ' . \gettype($sTokenValue)); + } + } else { + \SnappyMail\Log::notice('TOKENS', 'AUTH_SPEC_TOKEN_KEY invalid'); + } + } + if (!$this->oMainAuthAccount) { + Cookies::clear(Utils::SESSION_TOKEN); // \MailSo\Base\Http::StatusHeader(401); $this->Logout(true); // $sAdditionalMessage = $this->StaticI18N('SESSION_GONE'); @@ -300,6 +295,9 @@ trait UserAuth { $this->oAdditionalAuthAccount = false; $this->oMainAuthAccount = $oAccount; + if (!isset($_COOKIE['smctoken'])) { + Cookies::set('smctoken', \base64_encode(\random_bytes(16)), 0, false); + } static::SetAccountCookie(self::AUTH_SPEC_TOKEN_KEY, $oAccount); } @@ -315,13 +313,16 @@ trait UserAuth private static function GetSignMeToken(): ?array { - $sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY); + $sSignMeToken = Cookies::get(self::AUTH_SIGN_ME_TOKEN_KEY); if ($sSignMeToken) { + \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'decrypt'); $aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken); if (isset($aResult['e'], $aResult['u']) && \SnappyMail\UUID::isValid($aResult['u'])) { return $aResult; } \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'invalid'); + // Don't clear due to smctoken cookie missing at initialization and login checkbox +// Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY); } return null; } @@ -329,11 +330,9 @@ trait UserAuth private function SetSignMeToken(MainAccount $oAccount): void { $this->ClearSignMeData(); - $uuid = \SnappyMail\UUID::generate(); $data = \SnappyMail\Crypt::Encrypt($oAccount); - - Utils::SetCookie( + Cookies::set( self::AUTH_SIGN_ME_TOKEN_KEY, \SnappyMail\Crypt::EncryptUrlSafe([ 'e' => $oAccount->Email(), @@ -342,13 +341,7 @@ trait UserAuth ]), \time() + 3600 * 24 * 30 // 30 days ); - - $this->StorageProvider()->Put( - $oAccount, - StorageType::SIGN_ME, - $uuid, - $data[2] - ); + $this->StorageProvider()->Put($oAccount, StorageType::SIGN_ME, $uuid, $data[2]); } public function GetAccountFromSignMeToken(): ?MainAccount @@ -362,36 +355,33 @@ trait UserAuth StorageType::SIGN_ME, $aTokenData['u'] ); - if ($sAuthToken) { - $aAccountHash = \SnappyMail\Crypt::Decrypt([ - \array_key_last($aTokenData), - \base64_decode(\end($aTokenData)), - $sAuthToken - ]); - if (\is_array($aAccountHash)) { - $oAccount = MainAccount::NewInstanceFromTokenArray($this, $aAccountHash); - if ($oAccount) { - $this->CheckMailConnection($oAccount); - // Update lifetime - $this->SetSignMeToken($oAccount); - return $oAccount; - } - \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'has no account'); - } else { - \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'decrypt failed'); - } - } else { - \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, "server token not found for {$aTokenData['e']}/.sign_me/{$aTokenData['u']}"); + if (!$sAuthToken) { + throw new \RuntimeException("server token not found for {$aTokenData['e']}/.sign_me/{$aTokenData['u']}"); } + $aAccountHash = \SnappyMail\Crypt::Decrypt([ + \array_key_last($aTokenData), + \base64_decode(\end($aTokenData)), + $sAuthToken + ]); + if (!\is_array($aAccountHash)) { + throw new \RuntimeException('token decrypt failed'); + } + $oAccount = MainAccount::NewInstanceFromTokenArray($this, $aAccountHash); + if (!$oAccount) { + throw new \RuntimeException('token has no account'); + } + $this->imapConnect($oAccount); + // Update lifetime + $this->SetSignMeToken($oAccount); + return $oAccount; } catch (\Throwable $oException) { - \SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, $oException->getMessage()); + \SnappyMail\Log::warning(self::AUTH_SIGN_ME_TOKEN_KEY, $oException->getMessage()); + // Don't clear due to smctoken cookie missing at initialization and login checkbox +// $this->ClearSignMeData(); } } - - $this->ClearSignMeData(); - return null; } @@ -401,49 +391,31 @@ trait UserAuth if ($aTokenData) { $this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']); } - Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); + Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY); } /** * Logout methods */ - public function SetAuthLogoutToken(): void - { - \header('X-RainLoop-Action: Logout'); - Utils::SetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, \md5($_SERVER['REQUEST_TIME_FLOAT'])); - } - - public function GetSpecLogoutCustomMgsWithDeletion(): string - { - $sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, ''); - if (\strlen($sResult)) { - Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY); - } - - return $sResult; - } - - public function SetSpecLogoutCustomMgsWithDeletion(string $sMessage): void - { - Utils::SetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, $sMessage); - } - public function Logout(bool $bMain) : void { -// Utils::ClearCookie(Utils::SESSION_TOKEN); - Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); - $bMain && Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY); +// Cookies::clear(Utils::SESSION_TOKEN); + Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY); + $bMain && Cookies::clear(self::AUTH_SPEC_TOKEN_KEY); // TODO: kill SignMe data to prevent automatic login? } /** * @throws \RainLoop\Exceptions\ClientException */ - protected function CheckMailConnection(Account $oAccount, bool $bAuthLog = false): void + protected function imapConnect(Account $oAccount, bool $bAuthLog = false, \MailSo\Imap\ImapClient $oImapClient = null): void { try { - $oAccount->ImapConnectAndLoginHelper($this->Plugins(), $this->MailClient(), $this->Config()); + if (!$oImapClient) { + $oImapClient = $this->ImapClient(); + } + $oAccount->ImapConnectAndLogin($this->Plugins(), $oImapClient, $this->Config()); } catch (ClientException $oException) { throw $oException; } catch (\MailSo\Net\Exceptions\ConnectionException $oException) { 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 b5df87b4e..5b8cef13d 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ActionsAdmin.php @@ -7,6 +7,8 @@ use RainLoop\Exceptions\ClientException; class ActionsAdmin extends Actions { + use Actions\AdminDomains; + use Actions\AdminExtensions; public function DoAdminClearCache() : array { @@ -14,14 +16,14 @@ class ActionsAdmin extends Actions if (\is_dir(APP_PRIVATE_DATA . 'cache')) { \MailSo\Base\Utils::RecRmDir(APP_PRIVATE_DATA.'cache'); } - return $this->TrueResponse(__FUNCTION__); + return $this->TrueResponse(); } public function DoAdminSettingsGet() : array { $aConfig = $this->Config()->jsonSerialize(); unset($aConfig['version']); - return $this->DefaultResponse(__FUNCTION__, $aConfig); + return $this->DefaultResponse($aConfig); } public function DoAdminSettingsSet() : array @@ -32,13 +34,13 @@ class ActionsAdmin extends Actions $oConfig->Set($sSection, $sKey, $mValue); } } - return $this->DefaultResponse(__FUNCTION__, $oConfig->Save()); + return $this->DefaultResponse($oConfig->Save()); } public function DoAdminSettingsUpdate() : array { // sleep(3); -// return $this->DefaultResponse(__FUNCTION__, false); +// return $this->DefaultResponse(false); $this->IsAdminLoggined(); @@ -46,11 +48,11 @@ class ActionsAdmin extends Actions $self = $this; - $this->setConfigFromParams($oConfig, 'Language', 'webmail', 'language', 'string', function ($sLanguage) use ($self) { + $this->setConfigFromParams($oConfig, 'language', 'webmail', 'language', 'string', function ($sLanguage) use ($self) { return $self->ValidateLanguage($sLanguage, '', false); }); - $this->setConfigFromParams($oConfig, 'LanguageAdmin', 'webmail', 'language_admin', 'string', function ($sLanguage) use ($self) { + $this->setConfigFromParams($oConfig, 'languageAdmin', 'webmail', 'language_admin', 'string', function ($sLanguage) use ($self) { return $self->ValidateLanguage($sLanguage, '', true); }); @@ -58,25 +60,25 @@ class ActionsAdmin extends Actions return $self->ValidateTheme($sTheme); }); - $this->setConfigFromParams($oConfig, 'VerifySslCertificate', 'ssl', 'verify_certificate', 'bool'); - $this->setConfigFromParams($oConfig, 'AllowSelfSigned', 'ssl', 'allow_self_signed', 'bool'); + $this->setConfigFromParams($oConfig, 'useLocalProxyForExternalImages', 'labs', 'use_local_proxy_for_external_images', 'bool'); - $this->setConfigFromParams($oConfig, 'UseLocalProxyForExternalImages', 'labs', 'use_local_proxy_for_external_images', 'bool'); + $this->setConfigFromParams($oConfig, 'allowLanguagesOnSettings', 'webmail', 'allow_languages_on_settings', 'bool'); + $this->setConfigFromParams($oConfig, 'allowLanguagesOnLogin', 'login', 'allow_languages_on_login', 'bool'); + $this->setConfigFromParams($oConfig, 'attachmentLimit', 'webmail', 'attachment_size_limit', 'int'); - $this->setConfigFromParams($oConfig, 'AllowLanguagesOnSettings', 'webmail', 'allow_languages_on_settings', 'bool'); - $this->setConfigFromParams($oConfig, 'AllowLanguagesOnLogin', 'login', 'allow_languages_on_login', 'bool'); - $this->setConfigFromParams($oConfig, 'AttachmentLimit', 'webmail', 'attachment_size_limit', 'int'); + $this->setConfigFromParams($oConfig, 'loginDefaultDomain', 'login', 'default_domain', 'string'); - $this->setConfigFromParams($oConfig, 'LoginDefaultDomain', 'login', 'default_domain', 'string'); - - $this->setConfigFromParams($oConfig, 'ContactsEnable', 'contacts', 'enable', 'bool'); - $this->setConfigFromParams($oConfig, 'ContactsSync', 'contacts', 'allow_sync', 'bool'); - $this->setConfigFromParams($oConfig, 'ContactsPdoDsn', 'contacts', 'pdo_dsn', 'string'); - $this->setConfigFromParams($oConfig, 'ContactsPdoUser', 'contacts', 'pdo_user', 'string'); - $this->setConfigFromParams($oConfig, 'ContactsPdoPassword', 'contacts', 'pdo_password', 'dummy'); - - $this->setConfigFromParams($oConfig, 'ContactsPdoType', 'contacts', 'type', 'string', function ($sType) use ($self) { - return \RainLoop\Providers\AddressBook\PdoAddressBook::validPdoType($sType); + $this->setConfigFromParams($oConfig, 'contactsEnable', 'contacts', 'enable', 'bool'); + $this->setConfigFromParams($oConfig, 'contactsSync', 'contacts', 'allow_sync', 'bool'); + $this->setConfigFromParams($oConfig, 'contactsPdoDsn', 'contacts', 'pdo_dsn', 'string'); + $this->setConfigFromParams($oConfig, 'contactsPdoUser', 'contacts', 'pdo_user', 'string'); + $this->setConfigFromParams($oConfig, 'contactsPdoPassword', 'contacts', 'pdo_password', 'dummy'); + $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, 'contactsSuggestionsLimit', 'contacts', 'suggestions_limit', 'int'); + $this->setConfigFromParams($oConfig, 'contactsPdoType', 'contacts', 'type', 'string', function ($sType) use ($self) { + return Providers\AddressBook\PdoAddressBook::validPdoType($sType); }); $this->setConfigFromParams($oConfig, 'CapaAdditionalAccounts', 'webmail', 'allow_additional_accounts', 'bool'); @@ -86,17 +88,16 @@ class ActionsAdmin extends Actions $this->setConfigFromParams($oConfig, 'CapaUserBackground', 'webmail', 'allow_user_background', 'bool'); $this->setConfigFromParams($oConfig, 'CapaOpenPGP', 'security', 'openpgp', 'bool'); - $this->setConfigFromParams($oConfig, 'DetermineUserLanguage', 'login', 'determine_user_language', 'bool'); - $this->setConfigFromParams($oConfig, 'DetermineUserDomain', 'login', 'determine_user_domain', 'bool'); + $this->setConfigFromParams($oConfig, 'determineUserLanguage', 'login', 'determine_user_language', 'bool'); + $this->setConfigFromParams($oConfig, 'determineUserDomain', 'login', 'determine_user_domain', 'bool'); - $this->setConfigFromParams($oConfig, 'Title', 'webmail', 'title', 'string'); - $this->setConfigFromParams($oConfig, 'LoadingDescription', 'webmail', 'loading_description', 'string'); - $this->setConfigFromParams($oConfig, 'FaviconUrl', 'webmail', 'favicon_url', 'string'); + $this->setConfigFromParams($oConfig, 'title', 'webmail', 'title', 'string'); + $this->setConfigFromParams($oConfig, 'loadingDescription', 'webmail', 'loading_description', 'string'); + $this->setConfigFromParams($oConfig, 'faviconUrl', 'webmail', 'favicon_url', 'string'); - $this->setConfigFromParams($oConfig, 'TokenProtection', 'security', 'csrf_protection', 'bool'); - $this->setConfigFromParams($oConfig, 'EnabledPlugins', 'plugins', 'enable', 'bool'); + $this->setConfigFromParams($oConfig, 'pluginsEnable', 'plugins', 'enable', 'bool'); - return $this->DefaultResponse(__FUNCTION__, $oConfig->Save()); + return $this->DefaultResponse($oConfig->Save()); } /** @@ -107,7 +108,7 @@ class ActionsAdmin extends Actions $sLogin = trim($this->GetActionParam('Login', '')); $sPassword = $this->GetActionParam('Password', ''); - $this->Logger()->AddSecret($sPassword); + $this->logMask($sPassword); $totp = $this->Config()->Get('security', 'admin_totp', ''); @@ -123,10 +124,9 @@ class ActionsAdmin extends Actions throw new ClientException(Notifications::AuthError); } - $sToken = $this->getAdminToken(); - $this->setAdminAuthToken($sToken); + $sToken = $this->setAdminAuthToken(); - return $this->DefaultResponse(__FUNCTION__, $sToken ? $this->AppData(true) : false); + return $this->DefaultResponse($sToken ? $this->AppData(true) : false); } public function DoAdminLogout() : array @@ -135,8 +135,8 @@ class ActionsAdmin extends Actions if ($sAdminKey) { $this->Cacher(null, true)->Delete(KeyPathHelper::SessionAdminKey($sAdminKey)); } - Utils::ClearCookie(static::$AUTH_ADMIN_TOKEN_KEY); - return $this->TrueResponse(__FUNCTION__); + \SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY); + return $this->TrueResponse(); } public function DoAdminContactsTest() : array @@ -144,18 +144,27 @@ class ActionsAdmin extends Actions $this->IsAdminLoggined(); $oConfig = $this->Config(); - - $this->setConfigFromParams($oConfig, 'ContactsPdoDsn', 'contacts', 'pdo_dsn', 'string'); - $this->setConfigFromParams($oConfig, 'ContactsPdoUser', 'contacts', 'pdo_user', 'string'); - $this->setConfigFromParams($oConfig, 'ContactsPdoPassword', 'contacts', 'pdo_password', 'dummy'); - - $self = $this; - $this->setConfigFromParams($oConfig, 'ContactsPdoType', 'contacts', 'type', 'string', function ($sType) use ($self) { - return \RainLoop\Providers\AddressBook\PdoAddressBook::validPdoType($sType); + $this->setConfigFromParams($oConfig, 'PdoDsn', 'contacts', 'pdo_dsn', 'string'); + $this->setConfigFromParams($oConfig, 'PdoUser', 'contacts', 'pdo_user', 'string'); + $this->setConfigFromParams($oConfig, 'PdoPassword', 'contacts', 'pdo_password', 'dummy'); + $this->setConfigFromParams($oConfig, 'PdoType', 'contacts', 'type', 'string', function ($sType) { + return Providers\AddressBook\PdoAddressBook::validPdoType($sType); }); + $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'); - $sTestMessage = $this->AddressBookProvider(null, true)->Test(); - return $this->DefaultResponse(__FUNCTION__, array( + $sTestMessage = ''; + try { + $AddressBook = new Providers\AddressBook(new Providers\AddressBook\PdoAddressBook()); + $AddressBook->SetLogger($this->oLogger); + $sTestMessage = $AddressBook->Test(); + } catch (\Throwable $e) { + \SnappyMail\LOG::error('AddressBook', $e->getMessage()."\n".$e->getTraceAsString()); + $sTestMessage = $e->getMessage(); + } + + return $this->DefaultResponse(array( 'Result' => '' === $sTestMessage, 'Message' => \MailSo\Base\Utils::Utf8Clear($sTestMessage) )); @@ -169,11 +178,11 @@ class ActionsAdmin extends Actions $oConfig = $this->Config(); $sPassword = $this->GetActionParam('Password', ''); - $this->Logger()->AddSecret($sPassword); + $this->logMask($sPassword); - $sNewPassword = $this->GetActionParam('NewPassword', ''); + $sNewPassword = $this->GetActionParam('newPassword', ''); if (\strlen($sNewPassword)) { - $this->Logger()->AddSecret($sNewPassword); + $this->logMask($sNewPassword); } $passfile = APP_PRIVATE_DATA.'admin_password.txt'; @@ -196,253 +205,11 @@ class ActionsAdmin extends Actions $bResult = $oConfig->Save(); } - return $this->DefaultResponse(__FUNCTION__, $bResult + return $this->DefaultResponse($bResult ? array('Weak' => \is_file($passfile)) : false); } - public function DoAdminDomainLoad() : array - { - $this->IsAdminLoggined(); - - return $this->DefaultResponse(__FUNCTION__, - $this->DomainProvider()->Load($this->GetActionParam('Name', ''), false, false)); - } - - public function DoAdminDomainList() : array - { - $this->IsAdminLoggined(); - $bIncludeAliases = !empty($this->GetActionParam('IncludeAliases', '1')); - return $this->DefaultResponse(__FUNCTION__, $this->DomainProvider()->GetList($bIncludeAliases)); - } - - public function DoAdminDomainDelete() : array - { - $this->IsAdminLoggined(); - - return $this->DefaultResponse(__FUNCTION__, - $this->DomainProvider()->Delete((string) $this->GetActionParam('Name', ''))); - } - - public function DoAdminDomainDisable() : array - { - $this->IsAdminLoggined(); - - return $this->DefaultResponse(__FUNCTION__, $this->DomainProvider()->Disable( - (string) $this->GetActionParam('Name', ''), - '1' === (string) $this->GetActionParam('Disabled', '0') - )); - } - - public function DoAdminDomainSave() : array - { - $this->IsAdminLoggined(); - - $oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this); - - return $this->DefaultResponse(__FUNCTION__, - $oDomain ? $this->DomainProvider()->Save($oDomain) : false); - } - - public function DoAdminDomainAliasSave() : array - { - $this->IsAdminLoggined(); - - return $this->DefaultResponse(__FUNCTION__, $this->DomainProvider()->SaveAlias( - (string) $this->GetActionParam('Name', ''), - (string) $this->GetActionParam('Alias', '') - )); - } - - public function DoAdminDomainTest() : array - { - $this->IsAdminLoggined(); - - $bImapResult = false; - $sImapErrorDesc = ''; - $bSmtpResult = false; - $sSmtpErrorDesc = ''; - $bSieveResult = false; - $sSieveErrorDesc = ''; - - $iConnectionTimeout = 5; - - $oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this, 'test.example.com'); - if ($oDomain) - { - try - { - $oImapClient = new \MailSo\Imap\ImapClient(); - $oImapClient->SetLogger($this->Logger()); - $oImapClient->SetTimeOuts($iConnectionTimeout); - - $iTime = \microtime(true); - $oSettings = \MailSo\Net\ConnectSettings::fromArray($oDomain->ImapSettings()); - $oImapClient->Connect($oSettings); - - $sUsername = $this->GetActionParam('username', ''); - if ($sUsername) { - $aSASLMechanisms = []; - $oConfig = $this->Config(); - if ($oConfig->Get('labs', 'sasl_allow_scram_sha', false)) { - // https://github.com/the-djmaze/snappymail/issues/182 - \array_push($aSASLMechanisms, 'SCRAM-SHA3-512', 'SCRAM-SHA-512', 'SCRAM-SHA-256', 'SCRAM-SHA-1'); - } - if ($oConfig->Get('labs', 'sasl_allow_cram_md5', false)) { - $aSASLMechanisms[] = 'CRAM-MD5'; - } - if ($oConfig->Get('labs', 'sasl_allow_plain', true)) { - $aSASLMechanisms[] = 'PLAIN'; - } - $oImapClient->Login([ - 'Login' => $sUsername, - 'Password' => $this->GetActionParam('password', ''), - 'SASLMechanisms' => $aSASLMechanisms - ]); - } - - $oImapClient->Disconnect(); - $bImapResult = true; - } - catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - $sImapErrorDesc = $oException->getSocketMessage(); - if (empty($sImapErrorDesc)) - { - $sImapErrorDesc = $oException->getMessage(); - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - $sImapErrorDesc = $oException->getMessage(); - } - - if ($oDomain->OutUsePhpMail()) - { - $bSmtpResult = \MailSo\Base\Utils::FunctionCallable('mail'); - if (!$bSmtpResult) - { - $sSmtpErrorDesc = 'PHP: mail() function is undefined'; - } - } - else - { - try - { - $oSmtpClient = new \MailSo\Smtp\SmtpClient(); - $oSmtpClient->SetLogger($this->Logger()); - $oSmtpClient->SetTimeOuts($iConnectionTimeout); - - $iTime = \microtime(true); - $oSettings = \MailSo\Net\ConnectSettings::fromArray($oDomain->SmtpSettings()); - $oSmtpClient->Connect($oSettings, \MailSo\Smtp\SmtpClient::EhloHelper()); - - $oSmtpClient->Disconnect(); - $bSmtpResult = true; - } - catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - $sSmtpErrorDesc = $oException->getSocketMessage(); - if (empty($sSmtpErrorDesc)) - { - $sSmtpErrorDesc = $oException->getMessage(); - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - $sSmtpErrorDesc = $oException->getMessage(); - } - } - - if ($oDomain->UseSieve()) - { - try - { - $oSieveClient = new \MailSo\Sieve\ManageSieveClient(); - $oSieveClient->SetLogger($this->Logger()); - $oSieveClient->SetTimeOuts($iConnectionTimeout); - - $iTime = \microtime(true); - $oSettings = \MailSo\Net\ConnectSettings::fromArray($oDomain->SieveSettings()); - $oSieveClient->Connect($oSettings); - - $oSieveClient->Disconnect(); - $bSieveResult = true; - } - catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - $sSieveErrorDesc = $oException->getSocketMessage(); - if (empty($sSieveErrorDesc)) - { - $sSieveErrorDesc = $oException->getMessage(); - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - $sSieveErrorDesc = $oException->getMessage(); - } - } - else - { - $bSieveResult = true; - } - } - - return $this->DefaultResponse(__FUNCTION__, array( - 'Imap' => $bImapResult ? true : $sImapErrorDesc, - 'Smtp' => $bSmtpResult ? true : $sSmtpErrorDesc, - 'Sieve' => $bSieveResult ? true : $sSieveErrorDesc - )); - } - - public function DoAdminPHPExtensions() : array - { - $aResult = [ - [ - 'name' => 'PHP ' . PHP_VERSION, - 'loaded' => true - ] - ]; - 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) { - $aResult[] = [ - 'name' => $name, - 'loaded' => \extension_loaded(\strtolower($name)) - ]; - } - return $this->DefaultResponse(__FUNCTION__, $aResult); - } - - public function DoAdminPackagesList() : array - { - return $this->DefaultResponse(__FUNCTION__, \SnappyMail\Repository::getPackagesList()); - } - - public function DoAdminPackageDelete() : array - { - $sId = $this->GetActionParam('Id', ''); - $bResult = \SnappyMail\Repository::deletePackage($sId); - static::pluginEnable($sId, false); - return $this->DefaultResponse(__FUNCTION__, $bResult); - } - - public function DoAdminPackageInstall() : array - { - $sType = $this->GetActionParam('Type', ''); - $bResult = \SnappyMail\Repository::installPackage( - $sType, - $this->GetActionParam('Id', ''), - $this->GetActionParam('File', '') - ); - return $this->DefaultResponse(__FUNCTION__, $bResult ? - ('plugin' !== $sType ? array('Reload' => true) : true) : false); - } - // /?admin/Backup public function DoAdminBackup() : void { @@ -465,7 +232,7 @@ class ActionsAdmin extends Actions exit; } - public function DoAdminUpdateInfo() : array + public function DoAdminInfo() : array { $this->IsAdminLoggined(); @@ -474,7 +241,7 @@ class ActionsAdmin extends Actions $sVersion = empty($info->version) ? '' : $info->version; $bShowWarning = false; - if (!empty($info->warnings) && APP_VERSION !== APP_DEV_VERSION) { + if (!empty($info->warnings) && !SNAPPYMAIL_DEV) { foreach ($info->warnings as $sWarningVersion) { $sWarningVersion = \trim($sWarningVersion); @@ -498,228 +265,135 @@ class ActionsAdmin extends Actions $aWarnings[] = 'Can not edit: ' . APP_INDEX_ROOT_PATH . 'index.php'; } - return $this->DefaultResponse(__FUNCTION__, array( - 'Updatable' => \SnappyMail\Repository::canUpdateCore(), - 'Warning' => $bShowWarning, - 'Version' => $sVersion, - 'VersionCompare' => \version_compare(APP_VERSION, $sVersion), - 'Warnings' => $aWarnings - )); + $aResult = [ + 'system' => [ + 'load' => \is_callable('sys_getloadavg') ? \sys_getloadavg() : null + ], + 'core' => [ + 'updatable' => \SnappyMail\Repository::canUpdateCore(), + 'warning' => $bShowWarning, + 'version' => $sVersion, + 'versionCompare' => \version_compare(APP_VERSION, $sVersion), + 'warnings' => $aWarnings + ], + 'php' => [ + [ + 'name' => 'PHP ' . PHP_VERSION, + 'loaded' => true, + 'version' => PHP_VERSION + ], + [ + 'name' => 'PHP 64bit', + 'loaded' => PHP_INT_SIZE == 8, + 'version' => PHP_INT_SIZE + ] + ] + ]; + + 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) { + $aResult['php'][] = [ + 'name' => ('OpenSSL' === $name && \defined('OPENSSL_VERSION_TEXT')) ? OPENSSL_VERSION_TEXT : $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') + ]; + + return $this->DefaultResponse($aResult); } public function DoAdminUpgradeCore() : array { - return $this->DefaultResponse(__FUNCTION__, \SnappyMail\Upgrade::core()); - } - - public function DoAdminPluginDisable() : array - { - $this->IsAdminLoggined(); - - $sId = (string) $this->GetActionParam('Id', ''); - $bDisable = '1' === (string) $this->GetActionParam('Disabled', '1'); - - if (!$bDisable) - { - $oPlugin = $this->Plugins()->CreatePluginByName($sId); - if ($oPlugin) - { - $sValue = $oPlugin->Supported(); - if (\strlen($sValue)) - { - return $this->FalseResponse(__FUNCTION__, Notifications::UnsupportedPluginPackage, $sValue); - } - } - else - { - return $this->FalseResponse(__FUNCTION__, Notifications::InvalidPluginPackage); - } - } - - return $this->DefaultResponse(__FUNCTION__, $this->pluginEnable($sId, !$bDisable)); - } - - public function DoAdminPluginLoad() : array - { - $this->IsAdminLoggined(); - - $mResult = false; - $sId = (string) $this->GetActionParam('Id', ''); - - if (!empty($sId)) { - $oPlugin = $this->Plugins()->CreatePluginByName($sId); - if ($oPlugin) { - $mResult = array( - '@Object' => 'Object/Plugin', - 'Id' => $sId, - 'Name' => $oPlugin->Name(), - 'Readme' => $oPlugin->Description(), - 'Config' => array() - ); - - $aMap = $oPlugin->ConfigMap(); - if (\is_array($aMap)) { - $oConfig = $oPlugin->Config(); - foreach ($aMap as $oItem) { - if ($oItem) { - if ($oItem instanceof \RainLoop\Plugins\Property) { - if (PluginPropertyType::PASSWORD === $oItem->Type()) { - $oItem->SetValue(APP_DUMMY); - } else { - $oItem->SetValue($oConfig->Get('plugin', $oItem->Name(), '')); - } - $mResult['Config'][] = $oItem; - } else if ($oItem instanceof \RainLoop\Plugins\PropertyCollection) { - foreach ($oItem as $oSubItem) { - if ($oSubItem && $oSubItem instanceof \RainLoop\Plugins\Property) { - if (PluginPropertyType::PASSWORD === $oSubItem->Type()) { - $oSubItem->SetValue(APP_DUMMY); - } else { - $oSubItem->SetValue($oConfig->Get('plugin', $oSubItem->Name(), '')); - } - } - } - $mResult['Config'][] = $oItem; - } - } - } - } - } - } - - return $this->DefaultResponse(__FUNCTION__, $mResult); - } - - public function DoAdminPluginSettingsUpdate() : array - { - $this->IsAdminLoggined(); - - $mResult = false; - $sId = (string) $this->GetActionParam('Id', ''); - - if (!empty($sId)) - { - $oPlugin = $this->Plugins()->CreatePluginByName($sId); - if ($oPlugin) - { - $oConfig = $oPlugin->Config(); - $aMap = $oPlugin->ConfigMap(true); - if (\is_array($aMap)) - { - $aSettings = (array) $this->GetActionParam('Settings', []); - foreach ($aMap as $oItem) - { - $sKey = $oItem->Name(); - $sValue = $aSettings[$sKey] ?? $oConfig->Get('plugin', $sKey); - if (PluginPropertyType::PASSWORD !== $oItem->Type() || APP_DUMMY !== $sValue) - { - $mResultValue = null; - switch ($oItem->Type()) { - case PluginPropertyType::INT: - $mResultValue = (int) $sValue; - break; - case PluginPropertyType::BOOL: - $mResultValue = '1' === (string) $sValue; - break; - case PluginPropertyType::SELECTION: - if (is_array($oItem->DefaultValue()) && in_array($sValue, $oItem->DefaultValue())) - { - $mResultValue = (string) $sValue; - } - break; - case PluginPropertyType::PASSWORD: - case PluginPropertyType::STRING: - case PluginPropertyType::STRING_TEXT: - case PluginPropertyType::URL: - $mResultValue = (string) $sValue; - break; - } - - if (null !== $mResultValue) - { - $oConfig->Set('plugin', $sKey, $mResultValue); - } - } - } - } - - $mResult = $oConfig->Save(); - } - } - - if (!$mResult) - { - throw new ClientException(Notifications::CantSavePluginSettings); - } - - return $this->DefaultResponse(__FUNCTION__, true); + \header('Connection: close'); + return $this->DefaultResponse(\SnappyMail\Upgrade::core()); } public function DoAdminQRCode() : array { $user = (string) $this->GetActionParam('username', ''); $secret = (string) $this->GetActionParam('TOTP', ''); - $issuer = \rawurlencode(\RainLoop\API::Config()->Get('webmail', 'title', 'SnappyMail')); + $issuer = \rawurlencode(API::Config()->Get('webmail', 'title', 'SnappyMail')); $QR = \SnappyMail\QRCode::getMinimumQRCode( "otpauth://totp/{$issuer}:{$user}?secret={$secret}&issuer={$issuer}", // "otpauth://totp/{$user}?secret={$secret}", \SnappyMail\QRCode::ERROR_CORRECT_LEVEL_M ); - return $this->DefaultResponse(__FUNCTION__, $QR->__toString()); + return $this->DefaultResponse($QR->__toString()); } - private function setAdminAuthToken(string $sToken) : void +/* + public function AdminAppData(array &$aResult): void { - Utils::SetCookie(static::$AUTH_ADMIN_TOKEN_KEY, $sToken); - } + $oConfig = $this->oConfig; + $aResult['Auth'] = $this->IsAdminLoggined(false); + if ($aResult['Auth']) { + $aResult['adminLogin'] = (string)$oConfig->Get('security', 'admin_login', ''); + $aResult['adminTOTP'] = (string)$oConfig->Get('security', 'admin_totp', ''); + $aResult['pluginsEnable'] = (bool)$oConfig->Get('plugins', 'enable', false); - private function getAdminToken() : string - { - $sRand = \MailSo\Base\Utils::Sha1Rand(); - if (!$this->Cacher(null, true)->Set(KeyPathHelper::SessionAdminKey($sRand), \time())) - { - $this->oLogger->Write('Cannot store an admin token', - \LOG_WARNING); - return ''; - } + $aResult['loginDefaultDomain'] = $oConfig->Get('login', 'default_domain', ''); + $aResult['determineUserLanguage'] = (bool)$oConfig->Get('login', 'determine_user_language', true); + $aResult['determineUserDomain'] = (bool)$oConfig->Get('login', 'determine_user_domain', false); - return Utils::EncodeKeyValuesQ(array('token', $sRand)); - } + $aResult['supportedPdoDrivers'] = \RainLoop\Pdo\Base::getAvailableDrivers(); - private function pluginEnable(string $sName, bool $bEnable = true) : bool - { - if (!\strlen($sName)) - { - return false; - } + $aResult['contactsEnable'] = (bool)$oConfig->Get('contacts', 'enable', false); + $aResult['contactsSync'] = (bool)$oConfig->Get('contacts', 'allow_sync', false); + $aResult['contactsPdoType'] = Providers\AddressBook\PdoAddressBook::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); + $aResult['contactsPdoDsn'] = (string)$oConfig->Get('contacts', 'pdo_dsn', ''); + $aResult['contactsPdoType'] = (string)$oConfig->Get('contacts', 'type', ''); + $aResult['contactsPdoUser'] = (string)$oConfig->Get('contacts', 'pdo_user', ''); + $aResult['contactsPdoPassword'] = static::APP_DUMMY; + $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['contactsSuggestionsLimit'] = (int)$oConfig->Get('contacts', 'suggestions_limit', 20); - $oConfig = $this->Config(); + $aResult['faviconUrl'] = $oConfig->Get('webmail', 'favicon_url', ''); - $aEnabledPlugins = \SnappyMail\Repository::getEnabledPackagesNames(); + $aResult['weakPassword'] = \is_file(APP_PRIVATE_DATA.'admin_password.txt'); - $aNewEnabledPlugins = array(); - if ($bEnable) - { - $aNewEnabledPlugins = $aEnabledPlugins; - $aNewEnabledPlugins[] = $sName; - } - else - { - foreach ($aEnabledPlugins as $sPlugin) - { - if ($sName !== $sPlugin && \strlen($sPlugin)) - { - $aNewEnabledPlugins[] = $sPlugin; - } + $aResult['System']['languagesAdmin'] = \SnappyMail\L10n::getLanguages(true); + $aResult['languageAdmin'] = $this->ValidateLanguage($oConfig->Get('webmail', 'language_admin', 'en'), '', true); + $aResult['languageUsers'] = $this->ValidateLanguage($this->detectUserLanguage(true), '', true, true); + } else { + $passfile = APP_PRIVATE_DATA.'admin_password.txt'; + $sPassword = $oConfig->Get('security', 'admin_password', ''); + if (!$sPassword) { + $sPassword = \substr(\base64_encode(\random_bytes(16)), 0, 12); + Utils::saveFile($passfile, $sPassword . "\n"); +// \chmod($passfile, 0600); + $oConfig->SetPassword($sPassword); + $oConfig->Save(); } } + } +*/ - $oConfig->Set('plugins', 'enabled_list', \trim(\implode(',', \array_unique($aNewEnabledPlugins)), ' ,')); - - return $oConfig->Save(); + private function setAdminAuthToken() : string + { + $sRand = \MailSo\Base\Utils::Sha1Rand(); + if (!$this->Cacher(null, true)->Set(KeyPathHelper::SessionAdminKey($sRand), \time())) { + throw new \RuntimeException('Failed to store admin token'); + } + $sToken = Utils::EncodeKeyValuesQ(array('token', $sRand)); + if (!$sToken) { + throw new \RuntimeException('Failed to encode admin token'); + } + \SnappyMail\Cookies::set(static::$AUTH_ADMIN_TOKEN_KEY, $sToken); + return $sToken; } - private function setConfigFromParams(\RainLoop\Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void + private function setConfigFromParams(Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void { if ($this->HasActionParam($sParamName)) { $sValue = $this->GetActionParam($sParamName, ''); @@ -735,8 +409,8 @@ class ActionsAdmin extends Actions break; case 'dummy': - $sValue = (string) $this->GetActionParam($sParamName, APP_DUMMY); - if (APP_DUMMY !== $sValue) { + $sValue = (string) $this->GetActionParam($sParamName, static::APP_DUMMY); + if (static::APP_DUMMY !== $sValue) { $oConfig->Set($sConfigSector, $sConfigName, $sValue); } break; 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 f92d0cd74..16200a64e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php @@ -5,22 +5,10 @@ namespace RainLoop; abstract class Api { - public static function Handle() : bool - { - static $bOne = false; - if (!$bOne) - { - static::SetupDefaultMailSoConfig(); - $bOne = true; - } - return $bOne; - } - public static function Actions() : Actions { static $oActions = null; - if (null === $oActions) - { + if (null === $oActions) { $oActions = new Actions(); } @@ -36,16 +24,38 @@ abstract class Api \usleep(10000); $oConfig->Load(); } -// \ini_set('display_errors', 0); +// \ini_set('display_errors', '0'); if ($oConfig->Get('debug', 'enable', false)) { \error_reporting(E_ALL); -// \ini_set('display_errors', 1); - \ini_set('log_errors', 1); +// \ini_set('display_errors', '1'); + \ini_set('log_errors', '1'); } + \MailSo\Config::$BoundaryPrefix = \trim($oConfig->Get('labs', 'boundary_prefix', '')); } return $oConfig; } + public static function getCSP(string $sScriptNonce = null) : \SnappyMail\HTTP\CSP + { + $oConfig = static::Config(); + $CSP = new \SnappyMail\HTTP\CSP(\trim($oConfig->Get('security', 'content_security_policy', ''))); + $CSP->report = $oConfig->Get('security', 'csp_report', false); + $CSP->report_only = $oConfig->Get('debug', 'enable', false); // || SNAPPYMAIL_DEV + + // Allow https: due to remote images in e-mails or use proxy + if (!$oConfig->Get('security', 'use_local_proxy_for_external_images', '')) { + $CSP->add('img-src', 'https:'); + $CSP->add('img-src', 'http:'); + } + if ($sScriptNonce) { + $CSP->add('script-src', "'nonce-{$sScriptNonce}'"); + } + + static::Actions()->Plugins()->RunHook('main.content-security-policy', array($CSP)); + + return $CSP; + } + public static function Logger() : \MailSo\Log\Logger { static $oLogger = null; @@ -62,65 +72,6 @@ abstract class Api return $oLogger; } - protected static function SetupDefaultMailSoConfig() : void - { - if (\class_exists('MailSo\Config')) - { - \MailSo\Config::$MessageListFastSimpleSearch = - !!static::Config()->Get('imap', 'message_list_fast_simple_search', true); - - \MailSo\Config::$MessageListCountLimitTrigger = - (int) static::Config()->Get('imap', 'message_list_count_limit_trigger', 0); - - \MailSo\Config::$MessageListDateFilter = - (int) static::Config()->Get('imap', 'message_list_date_filter', 0); - - \MailSo\Config::$MessageListPermanentFilter = - \trim(static::Config()->Get('imap', 'message_list_permanent_filter', '')); - - \MailSo\Config::$MessageAllHeaders = - !!static::Config()->Get('imap', 'message_all_headers', false); - - \MailSo\Config::$LargeThreadLimit = - (int) static::Config()->Get('imap', 'large_thread_limit', 50); - - \MailSo\Config::$ImapTimeout = - (int) static::Config()->Get('imap', 'timeout', 300); - - \MailSo\Config::$BoundaryPrefix = - \trim(static::Config()->Get('labs', 'boundary_prefix', '')); - - $sSslCafile = static::Config()->Get('ssl', 'cafile', ''); - $sSslCapath = static::Config()->Get('ssl', 'capath', ''); - - Utils::$CookieDefaultPath = static::Config()->Get('labs', 'cookie_default_path', ''); - Utils::$CookieSameSite = static::Config()->Get('security', 'cookie_samesite', 'Strict'); - Utils::$CookieSecure = isset($_SERVER['HTTPS']) - || 'None' == Utils::$CookieSameSite - || !!static::Config()->Get('labs', 'cookie_default_secure', false); - - if (!empty($sSslCafile) || !empty($sSslCapath)) - { - \MailSo\Hooks::Add('Net.NetClient.StreamContextSettings/Filter', function ($aStreamContextSettings) use ($sSslCafile, $sSslCapath) { - if (isset($aStreamContextSettings['ssl']) && \is_array($aStreamContextSettings['ssl'])) - { - if (empty($aStreamContextSettings['ssl']['cafile']) && !empty($sSslCafile)) - { - $aStreamContextSettings['ssl']['cafile'] = $sSslCafile; - } - - if (empty($aStreamContextSettings['ssl']['capath']) && !empty($sSslCapath)) - { - $aStreamContextSettings['ssl']['capath'] = $sSslCapath; - } - } - }); - } - - \MailSo\Config::$CheckNewMessages = !!static::Config()->Get('labs', 'check_new_messages', true); - } - } - public static function Version() : string { return APP_VERSION; @@ -148,19 +99,16 @@ abstract class Api public static function ClearUserData(string $sEmail) : bool { - if (\strlen($sEmail)) - { + if (\strlen($sEmail)) { $sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail); $oStorageProvider = static::Actions()->StorageProvider(); - if ($oStorageProvider && $oStorageProvider->IsActive()) - { + if ($oStorageProvider && $oStorageProvider->IsActive()) { $oStorageProvider->DeleteStorage($sEmail); } $oAddressBookProvider = static::Actions()->AddressBookProvider(); - if ($oAddressBookProvider) - { + if ($oAddressBookProvider) { $oAddressBookProvider->DeleteAllContacts($sEmail); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php index 4d3f2190f..279ad0725 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/AbstractConfig.php @@ -2,42 +2,27 @@ namespace RainLoop\Config; -abstract class AbstractConfig implements \JsonSerializable +abstract class AbstractConfig implements \ArrayAccess, \JsonSerializable { - /** - * @var string - */ - private $sFile; + private string $sFile; - /** - * @var string - */ - private $sAdditionalFile = ''; + private string $sAdditionalFile = ''; - /** - * @var array - */ - private $aData; + private array $aData; - /** - * @var bool - */ - private $bUseApcCache; + private bool $bUseApcCache; - /** - * @var string - */ - private $sFileHeader; + private string $sFileHeader; public function __construct(string $sFileName, string $sFileHeader = '', string $sAdditionalFileName = '') { $this->sFile = \APP_PRIVATE_DATA.'configs/'.\trim($sFileName); $sAdditionalFileName = \trim($sAdditionalFileName); - if (\strlen($sAdditionalFileName)) { + if ($sAdditionalFileName) { $sAdditionalFileName = \APP_PRIVATE_DATA.'configs/'.$sAdditionalFileName; if (\file_exists($this->sAdditionalFile)) { - $this->sAdditionalFile = $this->sAdditionalFile; + $this->sAdditionalFile = $sAdditionalFileName; } } @@ -48,6 +33,27 @@ abstract class AbstractConfig implements \JsonSerializable \MailSo\Base\Utils::FunctionsCallable(array('apcu_fetch', 'apcu_store')); } + public function offsetExists($offset) : bool + { + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + $offset = \explode('/', $offset, 2); + $this->Get($offset[0], $offset[1]); + } + + public function offsetSet($offset, $value) : void + { + $offset = \explode('/', $offset, 2); + $this->Set($offset[0], $offset[1], $value); + } + + public function offsetUnset($offset) : void + { + } + protected abstract function defaultValues() : array; public function IsInited() : bool @@ -58,7 +64,7 @@ abstract class AbstractConfig implements \JsonSerializable #[\ReturnTypeWillChange] public function jsonSerialize() { - return $this->aData; + return $this->aData;; } /** @@ -78,8 +84,10 @@ abstract class AbstractConfig implements \JsonSerializable */ public function Set(string $sSectionKey, string $sParamKey, $mParamValue) : void { - if (isset($this->aData[$sSectionKey][$sParamKey][0])) - { + if (isset($this->aData[$sSectionKey][$sParamKey][0])) { + if (!\is_scalar($mParamValue)) { + $mParamValue = null; + } switch (\gettype($this->aData[$sSectionKey][$sParamKey][0])) { case 'boolean': @@ -96,13 +104,28 @@ abstract class AbstractConfig implements \JsonSerializable $this->aData[$sSectionKey][$sParamKey][0] = (string) $mParamValue; break; } - } - else if ('custom' === $sSectionKey) - { + } else if ('custom' === $sSectionKey) { $this->aData[$sSectionKey][$sParamKey] = array((string) $mParamValue); } } + public function getDecrypted(string $sSection, string $sName, $mDefault = null) + { + // $salt = \basename($this->sFile) not possible due to RainLoop\Plugins\Property + if (!empty($this->aData[$sSection][$sName][0])) try { + return \SnappyMail\Crypt::DecryptFromJSON($this->aData[$sSection][$sName][0], \APP_SALT); + } catch (\Throwable $e) { + } + return $mDefault; + } + + public function setEncrypted(string $sSectionKey, string $sParamKey, $mParamValue) : void + { + // $salt = \basename($this->sFile) not possible due to RainLoop\Plugins\Property + $mParamValue = \SnappyMail\Crypt::EncryptToJSON($mParamValue, \APP_SALT); + $this->Set($sSectionKey, $sParamKey, $mParamValue); + } + private function cacheKey() : string { return 'config:'.\sha1($this->sFile).':'.\sha1($this->sAdditionalFile).':'; @@ -110,24 +133,20 @@ abstract class AbstractConfig implements \JsonSerializable private function loadDataFromCache() : bool { - if ($this->bUseApcCache) - { + if ($this->bUseApcCache) { $iMTime = \filemtime($this->sFile); $iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0; $iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0; $iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0; - if (0 < $iMTime) - { + if (0 < $iMTime) { $sKey = $this->cacheKey(); $sTimeHash = \apcu_fetch($sKey.'time'); - if ($sTimeHash && $sTimeHash === \md5($iMTime.'/'.$iATime)) - { + if ($sTimeHash && $sTimeHash === \md5($iMTime.'/'.$iATime)) { $aFetchData = \apcu_fetch($sKey.'data'); - if (\is_array($aFetchData)) - { + if (\is_array($aFetchData)) { $this->aData = $aFetchData; return true; } @@ -140,16 +159,14 @@ abstract class AbstractConfig implements \JsonSerializable private function storeDataToCache() : bool { - if ($this->bUseApcCache) - { + if ($this->bUseApcCache) { $iMTime = \filemtime($this->sFile); $iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0; $iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0; $iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0; - if (0 < $iMTime) - { + if (0 < $iMTime) { $sKey = $this->cacheKey(); \apcu_store($sKey.'time', \md5($iMTime.'/'.$iATime)); @@ -164,8 +181,7 @@ abstract class AbstractConfig implements \JsonSerializable private function clearCache() : bool { - if ($this->bUseApcCache) - { + if ($this->bUseApcCache) { $sKey = $this->cacheKey(); \apcu_delete($sKey.'time'); @@ -179,22 +195,24 @@ abstract class AbstractConfig implements \JsonSerializable public function Load() : bool { - if (\file_exists($this->sFile) && \is_readable($this->sFile)) - { - if ($this->loadDataFromCache()) - { + $sFile = $this->sFile; + if (!\file_exists($sFile) && \str_ends_with($sFile, '.json')) { + $sFile = \str_replace('.json', '.ini', $sFile); + } + if (\file_exists($sFile) && \is_readable($sFile)) { + if ($this->loadDataFromCache()) { return true; } - $aData = \parse_ini_file($this->sFile, true); - if ($aData && \count($aData)) - { - foreach ($aData as $sSectionKey => $aSectionValue) - { - if (\is_array($aSectionValue)) - { - foreach ($aSectionValue as $sParamKey => $mParamValue) - { + if (\str_ends_with($sFile, '.json')) { + $aData = \json_decode(\file_get_contents($sFile), true); + } else { + $aData = \parse_ini_file($sFile, true); + } + if ($aData && \count($aData)) { + foreach ($aData as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + foreach ($aSectionValue as $sParamKey => $mParamValue) { $this->Set($sSectionKey, $sParamKey, $mParamValue); } } @@ -202,24 +220,23 @@ abstract class AbstractConfig implements \JsonSerializable unset($aData); - if (\file_exists($this->sAdditionalFile) && \is_readable($this->sAdditionalFile)) - { - $aSubData = \parse_ini_file($this->sAdditionalFile, true); - if ($aSubData && \count($aSubData)) - { - foreach ($aSubData as $sSectionKey => $aSectionValue) - { - if (\is_array($aSectionValue)) - { - foreach ($aSectionValue as $sParamKey => $mParamValue) - { + if (\file_exists($this->sAdditionalFile) && \is_readable($this->sAdditionalFile)) { + if (\str_ends_with($this->sAdditionalFile, '.json')) { + $aData = \json_decode(\file_get_contents($this->sAdditionalFile), true); + } else { + $aData = \parse_ini_file($this->sAdditionalFile, true); + } + if ($aData && \count($aData)) { + foreach ($aData as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + foreach ($aSectionValue as $sParamKey => $mParamValue) { $this->Set($sSectionKey, $sParamKey, $mParamValue); } } } } - unset($aSubData); + unset($aData); } $this->storeDataToCache(); @@ -233,43 +250,42 @@ abstract class AbstractConfig implements \JsonSerializable public function Save() : bool { - if (\file_exists($this->sFile) && !\is_writable($this->sFile)) - { + if (\file_exists($this->sFile) && !\is_writable($this->sFile)) { return false; } + if (\str_ends_with($this->sFile, '.json')) { + $this->clearCache(); + \RainLoop\Utils::saveFile($this->sFile, \json_encode($this, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + // Remove old .ini file + $sFile = \str_replace('.json', '.ini', $this->sFile); + \file_exists($sFile) && \unlink($sFile); + return true; + } + $sNewLine = "\n"; $aResultLines = array(); - foreach ($this->aData as $sSectionKey => $aSectionValue) - { - if (\is_array($aSectionValue)) - { + foreach ($this->aData as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { $aResultLines[] = ''; $aResultLines[] = '['.$sSectionKey.']'; $bFirst = true; - foreach ($aSectionValue as $sParamKey => $mParamValue) - { - if (\is_array($mParamValue)) - { - if (!empty($mParamValue[1])) - { - $sDesk = \str_replace("\r", '', $mParamValue[1]); - $aDesk = \explode("\n", $sDesk); - $aDesk = \array_map(function ($sLine) { - return '; '.$sLine; - }, $aDesk); - - if (!$bFirst) - { + foreach ($aSectionValue as $sParamKey => $mParamValue) { + if (\is_array($mParamValue)) { + // Add comments + if (!empty($mParamValue[1])) { + if (!$bFirst) { $aResultLines[] = ''; } - - $aResultLines[] = \implode($sNewLine, $aDesk); + foreach (\explode("\n", \str_replace("\r", '', $mParamValue[1])) as $sLine) { + $aResultLines[] = '; ' . $sLine; + } } + // Add value $bFirst = false; $sValue = '""'; 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 816625c8c..b9c92220c 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 @@ -4,7 +4,7 @@ namespace RainLoop\Config; class Application extends \RainLoop\Config\AbstractConfig { - private $aReplaceEnv = null; + private ?array $aReplaceEnv = null; public function __construct() { @@ -18,27 +18,45 @@ class Application extends \RainLoop\Config\AbstractConfig { $bResult = parent::Load(); + $max = \floatval($this->Get('security', 'max_sys_getloadavg', 0)); + if ($max && \is_callable('sys_getloadavg')) { + $load = \sys_getloadavg(); + if ($load && $load[0] > $max) { + \header('HTTP/1.1 503 Service Unavailable', true, 503); + \header('Retry-After: 120'); + exit("Mailserver too busy ({$load[0]}). Please try again later."); + } + } + $this->aReplaceEnv = null; if ((isset($_ENV) && \is_array($_ENV) && \count($_ENV)) || (isset($_SERVER) && \is_array($_SERVER) && \count($_SERVER))) { $sEnvNames = $this->Get('labs', 'replace_env_in_configuration', ''); - if (\strlen($sEnvNames)) - { + if (\strlen($sEnvNames)) { $this->aReplaceEnv = \explode(',', $sEnvNames); - if (\is_array($this->aReplaceEnv)) - { + if (\is_array($this->aReplaceEnv)) { $this->aReplaceEnv = \array_map('trim', $this->aReplaceEnv); $this->aReplaceEnv = \array_map('strtolower', $this->aReplaceEnv); } } } - if (!\is_array($this->aReplaceEnv) || 0 === \count($this->aReplaceEnv)) - { + if (!\is_array($this->aReplaceEnv) || !\count($this->aReplaceEnv)) { $this->aReplaceEnv = null; } + $sCipher = $this->Get('security', 'encrypt_cipher', ''); + if (!$sCipher || !\SnappyMail\Crypt::cipherSupported($sCipher)) { + $sCipher && \SnappyMail\Log::warning('Crypt', "OpenSSL no support for cipher '{$sCipher}'"); + $aCiphers = \SnappyMail\Crypt::listCiphers(); + $sCipher2 = $aCiphers ? $aCiphers[\array_rand($aCiphers)] : ''; + if ($sCipher !== $sCipher2) { + $this->Set('security', 'encrypt_cipher', $sCipher2); + $this->Save(); + } + } + return $bResult; } @@ -50,43 +68,50 @@ class Application extends \RainLoop\Config\AbstractConfig public function Get(string $sSection, string $sName, $mDefault = null) { $mResult = parent::Get($sSection, $sName, $mDefault); - if ($this->aReplaceEnv && \is_string($mResult)) - { + if ($this->aReplaceEnv && \is_string($mResult)) { $sKey = \strtolower($sSection.'.'.$sName); - if (\in_array($sKey, $this->aReplaceEnv) && false !== strpos($mResult, '$')) - { + if (\in_array($sKey, $this->aReplaceEnv) && false !== strpos($mResult, '$')) { $mResult = \preg_replace_callback('/\$([^\s]+)/', function($aMatch) { - - if (!empty($aMatch[0]) && !empty($aMatch[1])) - { - if (!empty($_ENV[$aMatch[1]])) - { + if (!empty($aMatch[0]) && !empty($aMatch[1])) { + if (!empty($_ENV[$aMatch[1]])) { + return $_ENV[$aMatch[1]]; + } + if (!empty($_SERVER[$aMatch[1]])) { return $_SERVER[$aMatch[1]]; } - - if (!empty($_SERVER[$aMatch[1]])) - { - return $_SERVER[$aMatch[1]]; - } - return $aMatch[0]; } - return ''; - }, $mResult); } } - return $mResult; } public function Set(string $sSectionKey, string $sParamKey, $mParamValue) : void { - if ('labs' === $sSectionKey && \str_contains($sParamKey, 'imap_')) { - // This is a workaround for the changed application structure - $sSectionKey = 'imap'; - $sParamKey = \str_replace('imap_', '', $sParamKey); + // Workarounds for the changed application structure + if ('labs' === $sSectionKey) { + if (\str_starts_with($sParamKey, 'imap_')) { + $sSectionKey = 'imap'; + $sParamKey = \str_replace('imap_', '', $sParamKey); + } + if (\str_starts_with($sParamKey, 'use_app_debug_')) { + $sSectionKey = 'debug'; + $sParamKey = \str_replace('use_app_debug_js', 'javascript', $sParamKey); + $sParamKey = \str_replace('use_app_debug_css', 'css', $sParamKey); + } + if ('cache_system_data' === $sParamKey) { + $sSectionKey = 'cache'; + $sParamKey = 'system_data'; + } + if ('force_https' === $sParamKey) { + $sSectionKey = 'security'; + } + if ('check_new_messages' === $sParamKey) { + $sSectionKey = 'imap'; + $sParamKey = 'fetch_new_messages'; + } } parent::Set($sSectionKey, $sParamKey, $mParamValue); } @@ -125,12 +150,6 @@ class Application extends \RainLoop\Config\AbstractConfig } $upload_max_filesize = $upload_max_filesize / 1024 / 1024; - $sCipher = 'aes-256-cbc-hmac-sha1'; - $aCiphers = \SnappyMail\Crypt::listCiphers(); - if (!\in_array($sCipher, $aCiphers)) { - $sCipher = $aCiphers[\array_rand($aCiphers)]; - } - return array( 'webmail' => array( @@ -155,7 +174,10 @@ class Application extends \RainLoop\Config\AbstractConfig 'message_read_delay' => array(5, 'Mark message read after N seconds'), 'attachment_size_limit' => array(\min($upload_max_filesize, 25), 'File size limit (MB) for file upload on compose screen -0 for unlimited.') +0 for unlimited.'), + + 'compress_output' => array(false, 'brotli or gzip compress the output. +Warning: only enable when server does not do this, else double compression errors occur') ), 'interface' => array( @@ -170,29 +192,40 @@ class Application extends \RainLoop\Config\AbstractConfig 'pdo_dsn' => array('host=127.0.0.1;port=3306;dbname=snappymail'), 'pdo_user' => array('root'), 'pdo_password' => array(''), - 'suggestions_limit' => array(30) + 'mysql_ssl_ca' => array('', 'PEM format certificate'), + 'mysql_ssl_verify' => array(true), + 'mysql_ssl_ciphers' => array('', 'HIGH'), + 'suggestions_limit' => array(20) ), 'security' => array( - 'csrf_protection' => array(true, - 'Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)'), + 'custom_server_signature' => array('SnappyMail'), + 'x_xss_protection_header' => array('1; mode=block'), - 'custom_server_signature' => array('SnappyMail'), - 'x_xss_protection_header' => array('1; mode=block'), + 'openpgp' => array(false), - 'openpgp' => array(false), + 'allow_admin_panel' => array(true, 'Access settings'), + 'admin_login' => array('admin', 'Login and password for web admin panel'), + 'admin_password' => array(''), + 'admin_totp' => array(''), + 'admin_panel_host' => array(''), + 'admin_panel_key' => array('admin'), - 'admin_login' => array('admin', 'Login and password for web admin panel'), - 'admin_password' => array(''), - 'admin_totp' => array(''), - 'allow_admin_panel' => array(true, 'Access settings'), - 'hide_x_mailer_header' => array(true), - 'admin_panel_host' => array(''), - 'admin_panel_key' => array('admin'), - 'content_security_policy' => array('', 'For example to allow all images use "img-src https:". More info at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives'), - 'csp_report' => array(false, 'Report CSP errors to PHP and/or SnappyMail Log'), - 'encrypt_cipher' => array($sCipher, 'A valid cipher method from https://php.net/openssl_get_cipher_methods'), - 'cookie_samesite' => array('Strict', 'Strict, Lax or None') + 'force_https' => array(false), + 'hide_x_mailer_header' => array(true), + 'max_sys_getloadavg' => array(0.0, 'https://en.m.wikipedia.org/wiki/Load_(computing)'), + 'content_security_policy' => array('', 'For example to allow all images use "img-src https:". More info at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives'), + 'csp_report' => array(false, 'Report CSP errors to PHP and/or SnappyMail Log'), + 'encrypt_cipher' => array('aes-256-cbc-hmac-sha1', 'A valid cipher method from https://php.net/openssl_get_cipher_methods'), + 'cookie_samesite' => array('Strict', 'Strict, Lax or None'), + 'secfetch_allow' => array('', 'Additional allowed Sec-Fetch combinations separated by ";". +For example: +* Allow iframe on same domain in any mode: dest=iframe,site=same-origin +* Allow navigate to iframe on same domain: mode=navigate,dest=iframe,site=same-origin +* Allow navigate to iframe on (sub)domain: mode=navigate,dest=iframe,site=same-site +* Allow navigate to iframe from any domain: mode=navigate,dest=iframe,site=cross-site + +Default is "site=same-origin;site=none"') ), 'admin_panel' => array( @@ -208,19 +241,19 @@ class Application extends \RainLoop\Config\AbstractConfig ), 'ssl' => array( - 'verify_certificate' => array(false, 'Require verification of SSL certificate used.'), - 'allow_self_signed' => array(true, 'Allow self-signed certificates. Requires verify_certificate.'), - 'security_level' => array(1, 'https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html'), - 'cafile' => array('', 'Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt)'), - 'capath' => array('', 'capath must be a correctly hashed certificate directory. (/etc/ssl/certs/)'), - 'client_cert' => array('', 'Location of client certificate file (pem format with private key) on local filesystem'), + 'verify_certificate' => array(true, 'Require verification of SSL certificate used.'), + 'allow_self_signed' => array(false, 'Allow self-signed certificates. Requires verify_certificate.'), + 'security_level' => array(1, 'https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html'), + 'cafile' => array('', 'Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt)'), + 'capath' => array('', 'capath must be a correctly hashed certificate directory. (/etc/ssl/certs/)'), + 'local_cert' => array('', 'Location of client certificate file (pem format with private key) on local filesystem'), + 'disable_compression' => array(true, 'This can help mitigate the CRIME attack vector.') ), 'capa' => array( 'quota' => array(true), - 'dangerous_actions' => array(true), - 'message_actions' => array(true), - 'attachments_actions' => array(true) + 'dangerous_actions' => array(true, 'Allow clear folder and delete messages without moving to trash'), + 'attachments_actions' => array(true, 'Allow download attachments as Zip (and optionally others)') ), 'login' => array( @@ -235,8 +268,8 @@ When this value is gethostname, the gethostname() value is used. 'allow_languages_on_login' => array(true, 'Allow language selection on webmail login screen'), - 'determine_user_language' => array(true), - 'determine_user_domain' => array(false), + 'determine_user_language' => array(true, 'Detect language from browser header `Accept-Language`'), + 'determine_user_domain' => array(false, 'Like default_domain but then HTTP_HOST/SERVER_NAME without www.'), 'login_lowercase' => array(true), @@ -252,26 +285,34 @@ Values: 'plugins' => array( 'enable' => array(false, 'Enable plugin support'), - 'enabled_list' => array('', 'List of enabled plugins'), + 'enabled_list' => array('', 'Comma-separated list of enabled plugins'), ), 'defaults' => array( 'view_editor_type' => array('Html', 'Editor mode used by default (Plain, Html)'), 'view_layout' => array(1, 'layout: 0 - no preview, 1 - side preview, 2 - bottom preview'), 'view_use_checkboxes' => array(true), + 'view_show_next_message' => array(true, 'Show next message when (re)move current message'), 'autologout' => array(30), 'view_html' => array(true), 'show_images' => array(false), + 'view_images' => array('ask', 'View external images: + "ask" - always ask + "match" - whitelist or ask + "always" - show always'), 'contacts_autosave' => array(true), 'mail_use_threads' => array(false), 'allow_draft_autosave' => array(true), - 'mail_reply_same_folder' => array(false) + 'mail_reply_same_folder' => array(false), + 'msg_default_action' => array(1, '1 - reply, 2 - reply all'), ), 'logs' => array( 'enable' => array(false, 'Enable logging'), + 'path' => array('', 'Path where log files will be stored'), + 'level' => array(4, 'Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest). 0 = Emergency 1 = Alert @@ -314,7 +355,8 @@ Examples: filename = "log-{date:Y-m-d}.txt" filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log" filename = "{user:email}-{date:Y-m-d}.txt" - filename = "syslog"'), + filename = "syslog" + filename = "stderr"'), 'auth_logging' => array(false, 'Enable auth logging in a separate file (for fail2ban)'), 'auth_logging_filename' => array('fail2ban/auth-{date:Y-m-d}.txt'), @@ -323,7 +365,11 @@ Examples: ), 'debug' => array( - 'enable' => array(false, 'Special option required for development purposes') + 'enable' => array(false, 'Special option required for development purposes'), + // use_app_debug_js + 'javascript' => array(false), + // use_app_debug_css + 'css' => array(false) ), 'cache' => array( @@ -332,6 +378,8 @@ Examples: Enables caching in the system'), + 'path' => array('', 'Path where cache files will be stored'), + 'index' => array('v1', 'Additional caching key. If changed, cache is purged'), 'fast_cache_driver' => array('files', 'Can be: files, APCU, memcache, redis (beta)'), @@ -340,60 +388,36 @@ Enables caching in the system'), 'http' => array(true, 'Browser-level cache. If enabled, caching is maintainted without using files'), 'http_expires' => array(3600, 'Browser-level cache time (seconds, Expires header)'), - 'server_uids' => array(true, 'Caching message UIDs when searching and sorting (threading)') + 'server_uids' => array(true, 'Caching message UIDs when searching and sorting (threading)'), + + 'system_data' => array(true) ), 'imap' => array( - 'use_sort' => array(true), 'use_force_selection' => array(false), - 'use_thread' => array(true), - 'use_move' => array(false), 'use_expunge_all_on_delete' => array(false), - 'body_text_limit' => array(555000), 'message_list_fast_simple_search' => array(true), - 'message_list_count_limit_trigger' => array(0), - 'message_list_date_filter' => array(0), 'message_list_permanent_filter' => array(''), 'message_all_headers' => array(false), - 'large_thread_limit' => array(50), - 'folder_list_limit' => array(200), 'show_login_alert' => array(true), - 'use_list_status' => array(true), - 'timeout' => array(300), - 'disable_metadata' => array(false), + 'fetch_new_messages' => array(true), ), 'labs' => array( - 'cache_system_data' => array(true), - 'date_from_headers' => array(true), - 'autocreate_system_folders' => array(false), + 'date_from_headers' => array(true, 'Display message RFC 2822 date and time header, instead of the arrival internal date.'), 'allow_message_append' => array(false), - 'login_fault_delay' => array(1), + 'login_fault_delay' => array(5, 'When login fails, wait N seconds before responding'), 'log_ajax_response_write_limit' => array(300), - 'allow_html_editor_biti_buttons' => array(false), - 'allow_ctrl_enter_on_compose' => array(true), - 'try_to_detect_hidden_images' => array(false), - 'use_app_debug_js' => array(false), - 'use_app_debug_css' => array(false), 'smtp_show_server_errors' => array(false), - 'smtp_timeout' => array(60), 'sieve_auth_plain_initial' => array(true), 'sieve_allow_fileinto_inbox' => array(false), - 'sieve_timeout' => array(10), - 'sasl_allow_plain' => array(true), - 'sasl_allow_scram_sha' => array(false), - 'sasl_allow_cram_md5' => array(false), - 'mail_func_clear_headers' => array(true), - 'mail_func_additional_parameters' => array(false), + 'mail_func_clear_headers' => array(true, 'PHP mail() remove To and Subject headers'), + 'mail_func_additional_parameters' => array(false, 'PHP mail() set -f emailaddress'), 'folders_spec_limit' => array(50), 'curl_proxy' => array(''), 'curl_proxy_auth' => array(''), - 'force_https' => array(false), 'custom_login_link' => array(''), 'custom_logout_link' => array(''), - 'allow_external_login' => array(false), - 'allow_external_sso' => array(false), - 'external_sso_key' => array(''), 'http_client_ip_check_proxy' => array(false), 'fast_cache_memcache_host' => array('127.0.0.1'), 'fast_cache_memcache_port' => array(11211), @@ -403,7 +427,6 @@ Enables caching in the system'), 'image_exif_auto_rotate' => array(false), 'cookie_default_path' => array(''), 'cookie_default_secure' => array(false), - 'check_new_messages' => array(true), 'replace_env_in_configuration' => array(''), 'boundary_prefix' => array(''), 'dev_email' => array(''), diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php index d848e333b..59f5d845b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Plugin.php @@ -4,20 +4,19 @@ namespace RainLoop\Config; class Plugin extends \RainLoop\Config\AbstractConfig { - /** - * @var array - */ - private $aMap = array(); + private array $aMap = array(); public function __construct(string $sPluginName, array $aMap = array()) { if (\count($aMap)) { $aResultMap = array(); - foreach ($aMap as /* @var $oProperty \RainLoop\Plugins\Property */ $oProperty) { - if ($oProperty) { - $mValue = $oProperty->DefaultValue(); - $sValue = \is_array($mValue) && isset($mValue[0]) ? $mValue[0] : $mValue; - $aResultMap[$oProperty->Name()] = array($sValue, ''); + foreach ($aMap as $oProperty) { + if ($oProperty instanceof \RainLoop\Plugins\Property) { + $mDefaultValue = $oProperty->DefaultValue(); + $aResultMap[$oProperty->Name()] = array( + \is_array($mDefaultValue) ? '' : $mDefaultValue, + '' + ); } } @@ -28,11 +27,27 @@ class Plugin extends \RainLoop\Config\AbstractConfig } } - parent::__construct('plugin-'.$sPluginName.'.ini', '; SnappyMail plugin ('.$sPluginName.')'); +// parent::__construct('plugin-'.$sPluginName.'.ini', '; SnappyMail plugin ('.$sPluginName.')'); + parent::__construct('plugin-'.$sPluginName.'.json'); } protected function defaultValues() : array { return $this->aMap; } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $aData = []; + foreach (parent::jsonSerialize() as $sSectionKey => $aSectionValue) { + if (\is_array($aSectionValue)) { + $aData[$sSectionKey] = []; + foreach ($aSectionValue as $sParamKey => $mParamValue) { + $aData[$sSectionKey][$sParamKey] = $mParamValue[0]; + } + } + } + return $aData; + } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php index 877c34c57..054b25335 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/Capa.php @@ -14,7 +14,6 @@ class Capa const IDENTITIES = 'Identities'; const CONTACTS = 'Contacts'; const QUOTA = 'Quota'; - const MESSAGE_ACTIONS = 'MessageActions'; const ATTACHMENTS_ACTIONS = 'AttachmentsActions'; const DANGEROUS_ACTIONS = 'DangerousActions'; const AUTOLOGOUT = ''; // Obsolete diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/PluginPropertyType.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/PluginPropertyType.php index 6b18a05f9..2cbf7a9d0 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/PluginPropertyType.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/PluginPropertyType.php @@ -12,5 +12,6 @@ class PluginPropertyType PASSWORD = 3, SELECTION = 4, BOOL = 5, - URL = 6; + URL = 6, + SELECT = 8; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/UploadError.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/UploadError.php index eb7160c36..20d7cc623 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/UploadError.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/UploadError.php @@ -4,27 +4,62 @@ namespace RainLoop\Enumerations; abstract class UploadError { - // UPLOAD_ERR_OK; There is no error, the file uploaded with success. - const NORMAL = 0; - // UPLOAD_ERR_INI_SIZE; The uploaded file exceeds the upload_max_filesize directive in php.ini. - const FILE_IS_TOO_BIG = 1; - // UPLOAD_ERR_FORM_SIZE; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form. -// const FORM_SIZE = 2; - // UPLOAD_ERR_PARTIAL; The uploaded file was only partially uploaded. - const FILE_PARTIALLY_UPLOADED = 3; - // UPLOAD_ERR_NO_FILE; No file was uploaded. - const FILE_NO_UPLOADED = 4; - // UPLOAD_ERR_NO_TMP_DIR; Missing a temporary folder. - const MISSING_TEMP_FOLDER = 6; - // UPLOAD_ERR_CANT_WRITE; Failed to write file to disk. - const FILE_ON_SAVING_ERROR = 7; - // UPLOAD_ERR_EXTENSION; A PHP extension stopped the file upload -// const EXTENSION = 8; - const FILE_TYPE = 98; const UNKNOWN = 99; const CONFIG_SIZE = 1001; const ON_SAVING = 1002; const EMPTY_FILES_DATA = 1003; + + private static $messages = [ + \UPLOAD_ERR_INI_SIZE => 'Filesize exceeds the upload_max_filesize directive in php.ini', + \UPLOAD_ERR_FORM_SIZE => 'Filesize exceeds the MAX_FILE_SIZE directive that was specified in the html form', + \UPLOAD_ERR_PARTIAL => 'File was only partially uploaded', + \UPLOAD_ERR_NO_FILE => 'No file was uploaded', + \UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder', + \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', + \UPLOAD_ERR_EXTENSION => 'File upload stopped by extension', + 98 => 'Invalid file type', + 99 => 'Unknown error', + 1001 => 'Filesize exceeds the config setting', + 1002 => 'Error saving file', + 1003 => 'File is empty' + ]; + + public static function getMessage(int $code): string + { + return isset(static::$messages[$code]) ? static::$messages[$code] : 0; + } + + public static function getUserMessage(int $iError, int &$iClientError): string + { + $iClientError = $iError; + switch ($iError) { + case \UPLOAD_ERR_OK: + case \UPLOAD_ERR_PARTIAL: + case \UPLOAD_ERR_NO_FILE: + case static::FILE_TYPE: + break; + + case \UPLOAD_ERR_INI_SIZE: + case \UPLOAD_ERR_FORM_SIZE: + case static::CONFIG_SIZE: + case static::EMPTY_FILES_DATA: + return 'File is too big'; + + case \UPLOAD_ERR_NO_TMP_DIR: + case \UPLOAD_ERR_CANT_WRITE: + case \UPLOAD_ERR_EXTENSION: + case static::ON_SAVING: + $iClientError = static::FILE_ON_SAVING_ERROR; + break; + + default: + $iClientError = static::UNKNOWN; + break; + } + + return static::getMessage($iClientError); + } + } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php b/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php index cd341aa10..509711925 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/KeyPathHelper.php @@ -4,12 +4,6 @@ namespace RainLoop; class KeyPathHelper { - - static public function PublicFile(string $sHash) : string - { - return '/Public/Files/'.\sha1($sHash).'/Data/'; - } - static public function SsoCacherKey(string $sSsoHash) : string { return '/Sso/Data/'.$sSsoHash.'/Login/'; @@ -30,11 +24,6 @@ class KeyPathHelper return '/LangCache/'.$sPluginsHash.'/'.$sLanguage.'/'.($bAdmim ? 'Admin' : 'App').'/'.APP_VERSION.'/'; } - static public function TemplatesCache(bool $bAdmin, string $sPluginsHash) : string - { - return '/TemplatesCache/'.$sPluginsHash.'/'.($bAdmin ? 'Admin' : 'App').'/'.APP_VERSION.'/'; - } - static public function PluginsJsCache(string $sPluginsHash) : string { return '/PluginsJsCache/'.$sPluginsHash.'/'.APP_VERSION.'/'; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php index 0adf20248..da028720a 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php @@ -8,60 +8,39 @@ use RainLoop\Exceptions\ClientException; abstract class Account implements \JsonSerializable { - /** - * @var string - */ - private $sEmail; + private string $sName = ''; - /** - * @var string - */ - private $sLogin; + private string $sEmail = ''; - /** - * @var string - */ - private $sPassword; + private string $sLogin = ''; - /** - * @var string - */ - private $sProxyAuthUser = ''; + private string $sPassword = ''; - /** - * @var string - */ - private $sProxyAuthPassword = ''; + private string $sSmtpLogin = ''; - /** - * @var \RainLoop\Model\Domain - */ - private $oDomain; + private string $sSmtpPassword = ''; + + private string $sProxyAuthUser = ''; + + private string $sProxyAuthPassword = ''; + + private Domain $oDomain; public function Email() : string { return $this->sEmail; } - public function ProxyAuthUser() : string + public function Name() : string { - return $this->sProxyAuthUser; - } - - public function ProxyAuthPassword() : string - { - return $this->sProxyAuthPassword; + return $this->sName; } public function IncLogin() : string { - $sLogin = $this->sLogin; - if ($this->oDomain->IncShortLogin()) - { - $sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($this->sLogin); - } - - return $sLogin; + return $this->oDomain->IncShortLogin() + ? \MailSo\Base\Utils::GetAccountNameFromEmail($this->sLogin) + : $this->sLogin; } public function IncPassword() : string @@ -71,23 +50,8 @@ abstract class Account implements \JsonSerializable public function OutLogin() : string { - $sLogin = $this->sLogin; - if ($this->oDomain->OutShortLogin()) - { - $sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($this->sLogin); - } - - return $sLogin; - } - - public function Login() : string - { - return $this->IncLogin(); - } - - public function Password() : string - { - return $this->IncPassword(); + $sSmtpLogin = $this->sSmtpLogin ?: $this->sLogin; + return $this->oDomain->OutShortLogin() ? \MailSo\Base\Utils::GetAccountNameFromEmail($sSmtpLogin) : $sSmtpLogin; } public function Domain() : Domain @@ -99,7 +63,7 @@ abstract class Account implements \JsonSerializable { return \sha1(\implode(APP_SALT, [ $this->sEmail, - APP_VERSION + $this->sLogin, // \json_encode($this->Domain()), // $this->sPassword ])); @@ -110,6 +74,11 @@ abstract class Account implements \JsonSerializable $this->sPassword = $sPassword; } + public function SetSmtpPassword(string $sPassword) : void + { + $this->sSmtpLogin = $sPassword; + } + public function SetProxyAuthUser(string $sProxyAuthUser) : void { $this->sProxyAuthUser = $sProxyAuthUser; @@ -123,15 +92,27 @@ abstract class Account implements \JsonSerializable #[\ReturnTypeWillChange] public function jsonSerialize() { - return array( - 'account', // 0 - $this->sEmail, // 1 - $this->sLogin, // 2 - $this->sPassword, // 3 - '', // 4 sClientCert - $this->sProxyAuthUser, // 5 - $this->sProxyAuthPassword // 6 - ); + $result = [ +// 'account', // 0 + 'email' => $this->sEmail, // 1 + 'login' => $this->sLogin, // 2 + 'pass' => $this->sPassword, // 3 +// '', // 4 sClientCert + 'name' => $this->sName + ]; + if ($this->sSmtpLogin && $this->sSmtpPassword) { + $result['smtp'] = [ + 'user' => $this->sSmtpLogin, + 'pass' => $this->sSmtpPassword + ]; + } + if ($this->sProxyAuthUser && $this->sProxyAuthPassword) { + $result['proxy'] = [ + 'user' => $this->sProxyAuthUser, // 5 + 'pass' => $this->sProxyAuthPassword // 6 + ]; + } + return $result; } public static function NewInstanceFromCredentials(\RainLoop\Actions $oActions, @@ -165,111 +146,138 @@ abstract class Account implements \JsonSerializable return $oAccount; } + /** + * Converts old numeric array to new associative array + */ + public static function convertArray(array $aAccount) : array + { + if (isset($aAccount['email'])) { + return $aAccount; + } + if (empty($aAccount[0]) || 'account' != $aAccount[0] || 7 > \count($aAccount)) { + return []; + } + $aResult = [ + 'email' => $aAccount[1] ?: '', + 'login' => $aAccount[2] ?: '', + 'pass' => $aAccount[3] ?: '' + ]; + if ($aAccount[5] && $aAccount[6]) { + $aResult['proxy'] = [ + 'user' => $aAccount[5], + 'pass' => $aAccount[6] + ]; + } + return $aResult; + } + public static function NewInstanceFromTokenArray( \RainLoop\Actions $oActions, array $aAccountHash, bool $bThrowExceptionOnFalse = false): ?self { - if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 <= \count($aAccountHash)) { + $oAccount = null; + $aAccountHash = static::convertArray($aAccountHash); + if (!empty($aAccountHash['email']) && 3 <= \count($aAccountHash)) { $oAccount = static::NewInstanceFromCredentials( $oActions, - $aAccountHash[1] ?: '', - $aAccountHash[2] ?: '', - $aAccountHash[3] ?: '', + $aAccountHash['email'], + $aAccountHash['login'], + $aAccountHash['pass'], $bThrowExceptionOnFalse ); - if ($oAccount) { - // init proxy user/password - if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) { - $oAccount->SetProxyAuthUser($aAccountHash[5]); - $oAccount->SetProxyAuthPassword($aAccountHash[6]); + if (isset($aAccountHash['name'])) { + $oAccount->sName = $aAccountHash['name']; + } + $oActions->logMask($oAccount->sPassword); + // init smtp user/password + if (isset($aAccountHash['smtp'])) { + $oAccount->sSmtpLogin = $aAccountHash['smtp']['user']; + $oAccount->sSmtpPassword = $aAccountHash['smtp']['pass']; + $oActions->logMask($oAccount->sSmtpPassword); + } + // init proxy user/password + if (isset($aAccountHash['proxy'])) { + $oAccount->sProxyAuthUser = $aAccountHash['proxy']['user']; + $oAccount->sProxyAuthPassword = $aAccountHash['proxy']['pass']; + $oActions->logMask($oAccount->sProxyAuthPassword); } - - $oActions->Logger()->AddSecret($oAccount->Password()); - $oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword()); - - return $oAccount; } } - return null; + return $oAccount; } - public function ImapConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Mail\MailClient $oMailClient, \RainLoop\Config\Application $oConfig) : bool + // Deprecated + public function ImapConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Imap\ImapClient $oImapClient, \RainLoop\Config\Application $oConfig) : bool { - $oImapClient = $oMailClient->ImapClient(); - $oImapClient->__FORCE_SELECT_ON_EXAMINE__ = !!$oConfig->Get('imap', 'use_force_selection'); - $oImapClient->__DISABLE_METADATA = !!$oConfig->Get('imap', 'disable_metadata'); - - $aCredentials = \array_merge( - $this->Domain()->ImapSettings(), - array( - 'Login' => $this->IncLogin(), - 'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false), - 'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true), - 'ClientCert' => \trim($oConfig->Get('ssl', 'client_cert', '')) - ) - ); - - $oPlugins->RunHook('imap.before-connect', array($this, $oImapClient, &$aCredentials)); - if ($aCredentials['UseConnect']) { - $oSettings = \MailSo\Net\ConnectSettings::fromArray($aCredentials); - $oImapClient->Connect($oSettings); - } - $oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $aCredentials)); - - return $this->netClientLogin($oImapClient, $oConfig, $oPlugins, $aCredentials); + return $this->ImapConnectAndLogin($oPlugins, $oImapClient, $oConfig); } - - public function SmtpConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Smtp\SmtpClient $oSmtpClient, \RainLoop\Config\Application $oConfig, bool &$bUsePhpMail = false) : bool + public function ImapConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Imap\ImapClient $oImapClient, \RainLoop\Config\Application $oConfig) : bool { - $aCredentials = \array_merge( - $this->Domain()->SmtpSettings(), - array( - 'UseConnect' => !$bUsePhpMail, - 'UsePhpMail' => $bUsePhpMail, - 'Login' => $this->OutLogin(), - 'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false), - 'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true) - ) - ); + $oSettings = $this->Domain()->ImapSettings(); + $oSettings->timeout = \max($oSettings->timeout, (int) $oConfig->Get('imap', 'timeout', $oSettings->timeout)); + $oSettings->Login = $this->IncLogin(); - $oPlugins->RunHook('smtp.before-connect', array($this, $oSmtpClient, &$aCredentials)); - $bUsePhpMail = $aCredentials['UsePhpMail']; - $aCredentials['UseAuth'] = $aCredentials['UseAuth'] && !$aCredentials['UsePhpMail']; + $oSettings->expunge_all_on_delete |= !!$oConfig->Get('imap', 'use_expunge_all_on_delete', false); + $oSettings->fast_simple_search = !(!$oSettings->fast_simple_search || !$oConfig->Get('imap', 'message_list_fast_simple_search', true)); + $oSettings->fetch_new_messages = !(!$oSettings->fetch_new_messages || !$oConfig->Get('imap', 'fetch_new_messages', true)); + $oSettings->force_select |= !!$oConfig->Get('imap', 'use_force_selection', false); + $oSettings->message_all_headers |= !!$oConfig->Get('imap', 'message_all_headers', false); + $oSettings->search_filter = $oSettings->search_filter ?: \trim($oConfig->Get('imap', 'message_list_permanent_filter', '')); +// $oSettings->body_text_limit = \min($oSettings->body_text_limit, (int) $oConfig->Get('imap', 'body_text_limit', 50)); +// $oSettings->thread_limit = \min($oSettings->thread_limit, (int) $oConfig->Get('imap', 'large_thread_limit', 50)); - if ($aCredentials['UseConnect'] && !$aCredentials['UsePhpMail']) { - $oSettings = \MailSo\Net\ConnectSettings::fromArray($aCredentials); - $oSmtpClient->Connect($oSettings, $aCredentials['Ehlo']); - } - $oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $aCredentials)); + $oImapClient->Settings = $oSettings; - return $this->netClientLogin($oSmtpClient, $oConfig, $oPlugins, $aCredentials); + $oPlugins->RunHook('imap.before-connect', array($this, $oImapClient, $oSettings)); + $oImapClient->Connect($oSettings); + $oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $oSettings)); + + $oSettings->Password = $this->IncPassword(); + return $this->netClientLogin($oImapClient, $oPlugins); } - public function SieveConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Sieve\ManageSieveClient $oSieveClient, \RainLoop\Config\Application $oConfig) + public function SmtpConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Smtp\SmtpClient $oSmtpClient) : bool { - $aCredentials = \array_merge( - $this->Domain()->SieveSettings(), - array( - 'Login' => $this->IncLogin(), - 'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false), - 'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true), - 'InitialAuthPlain' => !!$oConfig->Get('labs', 'sieve_auth_plain_initial', true) - ) - ); + $oSettings = $this->Domain()->SmtpSettings(); + $oSettings->Login = $this->OutLogin(); + $oSettings->Ehlo = \MailSo\Smtp\SmtpClient::EhloHelper(); - $oPlugins->RunHook('sieve.before-connect', array($this, $oSieveClient, &$aCredentials)); - if ($aCredentials['UseConnect']) { - $oSettings = \MailSo\Net\ConnectSettings::fromArray($aCredentials); - $oSieveClient->Connect($oSettings); + $oSmtpClient->Settings = $oSettings; + + $oPlugins->RunHook('smtp.before-connect', array($this, $oSmtpClient, $oSettings)); + if ($oSettings->usePhpMail) { + $oSettings->useAuth = false; + return true; } - $oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $aCredentials)); - - return $this->netClientLogin($oSieveClient, $oConfig, $oPlugins, $aCredentials); + $oSmtpClient->Connect($oSettings); + $oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $oSettings)); +/* + if ($this->oDomain->OutAskCredentials() && !($this->sSmtpPassword && $this->sSmtpLogin)) { + throw new RequireCredentialsException + } +*/ + $oSettings->Password = $this->sSmtpPassword ?: $this->sPassword; + return $this->netClientLogin($oSmtpClient, $oPlugins); } - private function netClientLogin(\MailSo\Net\NetClient $oClient, \RainLoop\Config\Application $oConfig, \RainLoop\Plugins\Manager $oPlugins, array $aCredentials) : bool + public function SieveConnectAndLogin(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Sieve\SieveClient $oSieveClient, \RainLoop\Config\Application $oConfig) + { + $oSettings = $this->Domain()->SieveSettings(); + $oSettings->Login = $this->IncLogin(); + + $oSieveClient->Settings = $oSettings; + + $oPlugins->RunHook('sieve.before-connect', array($this, $oSieveClient, $oSettings)); + $oSieveClient->Connect($oSettings); + $oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $oSettings)); + + $oSettings->Password = $this->IncPassword(); + return $this->netClientLogin($oSieveClient, $oPlugins); + } + + private function netClientLogin(\MailSo\Net\NetClient $oClient, \RainLoop\Plugins\Manager $oPlugins) : bool { /* $encrypted = !empty(\stream_get_meta_data($oClient->ConnectionResource())['crypto']); @@ -280,35 +288,15 @@ abstract class Account implements \JsonSerializable [cipher_version] => TLSv1.3 ) */ - /** - * TODO: move these to Admin -> Domains -> per Domain management? - */ - $aSASLMechanisms = []; - if ($oConfig->Get('labs', 'sasl_allow_scram_sha', false)) { - // https://github.com/the-djmaze/snappymail/issues/182 - \array_push($aSASLMechanisms, 'SCRAM-SHA3-512', 'SCRAM-SHA-512', 'SCRAM-SHA-256', 'SCRAM-SHA-1'); - } - if ($oConfig->Get('labs', 'sasl_allow_cram_md5', false)) { - $aSASLMechanisms[] = 'CRAM-MD5'; - } - if ($oConfig->Get('labs', 'sasl_allow_plain', true)) { - $aSASLMechanisms[] = 'PLAIN'; - } - $aCredentials = \array_merge( - $aCredentials, - array( - 'Password' => $this->Password(), - 'ProxyAuthUser' => $this->ProxyAuthUser(), - 'ProxyAuthPassword' => $this->ProxyAuthPassword(), - 'SASLMechanisms' => $aSASLMechanisms - ) - ); + $oSettings = $oClient->Settings; + $oSettings->ProxyAuthUser = $this->sProxyAuthUser; + $oSettings->ProxyAuthPassword = $this->sProxyAuthPassword; $client_name = \strtolower($oClient->getLogName()); - $oPlugins->RunHook("{$client_name}.before-login", array($this, $oClient, &$aCredentials)); - $bResult = $aCredentials['UseAuth'] && $oClient->Login($aCredentials); - $oPlugins->RunHook("{$client_name}.after-login", array($this, $oClient, $bResult, $aCredentials)); + $oPlugins->RunHook("{$client_name}.before-login", array($this, $oClient, $oSettings)); + $bResult = !$oSettings->useAuth || $oClient->Login($oSettings); + $oPlugins->RunHook("{$client_name}.after-login", array($this, $oClient, $bResult, $oSettings)); return $bResult; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php index fb03624a5..25709c884 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php @@ -14,16 +14,31 @@ class AdditionalAccount extends Account public function Hash() : string { - return \md5(parent::Hash() . $this->ParentEmail()); + return \sha1(parent::Hash() . $this->ParentEmail()); + } + + public static function convertArray(array $aAccount) : array + { + $aResult = parent::convertArray($aAccount); + $iCount = \count($aAccount); + if ($aResult && 7 < $iCount && 9 >= $iCount) { + $aResult['hmac'] = \array_pop($aAccount); + } + return $aResult; } public function asTokenArray(MainAccount $oMainAccount) : array { $sHash = $oMainAccount->CryptKey(); $aData = $this->jsonSerialize(); - $aData[3] = \SnappyMail\Crypt::EncryptUrlSafe($aData[3], $sHash); // sPassword - $aData[6] = \SnappyMail\Crypt::EncryptUrlSafe($aData[6], $sHash); // sProxyAuthPassword - $aData[] = \hash_hmac('sha1', $aData[3], $sHash); + $aData['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['pass'], $sHash); // sPassword + if (!empty($aData['smtp']['pass'])) { + $aData['smtp']['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['smtp']['pass'], $sHash); + } + if (!empty($aData['proxy']['pass'])) { + $aData['proxy']['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['proxy']['pass'], $sHash); // sProxyAuthPassword + } + $aData['hmac'] = \hash_hmac('sha1', $aData['pass'], $sHash); return $aData; } @@ -32,13 +47,29 @@ class AdditionalAccount extends Account array $aAccountHash, bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/ { - $iCount = \count($aAccountHash); - if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 <= $iCount && 9 >= $iCount) { + $aAccountHash = static::convertArray($aAccountHash); + if (!empty($aAccountHash['email'])) { $sHash = $oActions->getMainAccountFromToken()->CryptKey(); - $sPasswordHMAC = (7 < $iCount) ? \array_pop($aAccountHash) : null; - if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash[3], $sHash)) { - $aAccountHash[3] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[3], $sHash); - $aAccountHash[6] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[6], $sHash); + // hmac only set when asTokenArray() was used + $sPasswordHMAC = $aAccountHash['hmac'] ?? null; + if ($sPasswordHMAC) { + if ($sPasswordHMAC === \hash_hmac('sha1', $aAccountHash['pass'], $sHash)) { + $aAccountHash['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['pass'], $sHash); + if (!empty($aData['smtp']['pass'])) { + $aAccountHash['smtp']['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['smtp']['pass'], $sHash); + } + if (!empty($aData['proxy']['pass'])) { + $aAccountHash['proxy']['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['proxy']['pass'], $sHash); + } + } else { + $aAccountHash['pass'] = ''; + if (!empty($aData['smtp']['pass'])) { + $aAccountHash['smtp']['pass'] = ''; + } + if (!empty($aData['proxy']['pass'])) { + $aAccountHash['proxy']['pass'] = ''; + } + } } return parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php index 1a34ff223..16479d7f0 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Domain.php @@ -4,193 +4,81 @@ namespace RainLoop\Model; use MailSo\Net\Enumerations\ConnectionSecurityType; +// \SnappyMail\IDN::toAscii( +// \SnappyMail\IDN::toUtf8( + class Domain implements \JsonSerializable { - /** - * @var string - */ - private $sName; + private string $Name; - /** - * @var string - */ - private $sIncHost; + private \MailSo\Imap\Settings $IMAP; - /** - * @var int - */ - private $iIncPort; + private \MailSo\Smtp\Settings $SMTP; - /** - * @var int - */ - private $iIncSecure; + private \MailSo\Sieve\Settings $Sieve; - /** - * @var bool - */ - private $bIncShortLogin; + private string $whiteList = ''; - /** - * @var string - */ - private $sOutHost; - - /** - * @var int - */ - private $iOutPort; - - /** - * @var int - */ - private $iOutSecure; - - /** - * @var bool - */ - private $bOutShortLogin; - - /** - * @var bool - */ - private $bOutAuth; - - /** - * @var bool - */ - private $bOutSetSender; - - /** - * @var bool - */ - private $bOutUsePhpMail; - - /** - * @var bool - */ - private $bUseSieve; - - /** - * @var string - */ - private $sSieveHost; - - /** - * @var int - */ - private $iSievePort; - - /** - * @var int - */ - private $iSieveSecure; - - /** - * @var string - */ - private $sWhiteList; - - /** - * @var string - */ - private $sAliasName = ''; + private string $aliasName = ''; function __construct(string $sName) { - $this->sName = $sName; + $this->Name = $sName; + + $this->IMAP = new \MailSo\Imap\Settings; + $this->SMTP = new \MailSo\Smtp\Settings; + $this->Sieve = new \MailSo\Sieve\Settings; } /** - * See ToIniString() for valid values + * Used by old ToIniString() */ public static function fromIniArray(string $sName, array $aDomain) : ?self { $oDomain = null; - - if (\strlen($sName) && \strlen($aDomain['imap_host']) && \strlen($aDomain['imap_port'])) - { + if (\strlen($sName) && \strlen($aDomain['imap_host'])) { $oDomain = new self($sName); - $oDomain->sIncHost = (string) $aDomain['imap_host']; - $oDomain->iIncPort = (int) $aDomain['imap_port']; - $oDomain->iIncSecure = self::StrConnectionSecurityTypeToCons(empty($aDomain['imap_secure']) ? '' : $aDomain['imap_secure']); - $oDomain->bIncShortLogin = !empty($aDomain['imap_short_login']); + $oDomain->IMAP->host = \SnappyMail\IDN::toUtf8($aDomain['imap_host']); + $oDomain->IMAP->port = (int) $aDomain['imap_port']; + $oDomain->IMAP->type = self::StrConnectionSecurityTypeToCons($aDomain['imap_secure'] ?? ''); + $oDomain->IMAP->shortLogin = !empty($aDomain['imap_short_login']); - $oDomain->bUseSieve = !empty($aDomain['sieve_use']); - $oDomain->sSieveHost = empty($aDomain['sieve_host']) ? '' : (string) $aDomain['sieve_host']; - $oDomain->iSievePort = empty($aDomain['sieve_port']) ? 4190 : (int) $aDomain['sieve_port']; - $oDomain->iSieveSecure = self::StrConnectionSecurityTypeToCons(empty($aDomain['sieve_secure']) ? '' : $aDomain['sieve_secure']); + $oDomain->Sieve->enabled = !empty($aDomain['sieve_use']); + $oDomain->Sieve->host = \SnappyMail\IDN::toUtf8($aDomain['sieve_host']); + $oDomain->Sieve->port = (int) ($aDomain['sieve_port'] ?? 4190);; + $oDomain->Sieve->type = self::StrConnectionSecurityTypeToCons($aDomain['sieve_secure'] ?? ''); - $oDomain->sOutHost = empty($aDomain['smtp_host']) ? '' : (string) $aDomain['smtp_host']; - $oDomain->iOutPort = empty($aDomain['smtp_port']) ? 25 : (int) $aDomain['smtp_port']; - $oDomain->iOutSecure = self::StrConnectionSecurityTypeToCons(empty($aDomain['smtp_secure']) ? '' : $aDomain['smtp_secure']); - $oDomain->bOutShortLogin = !empty($aDomain['smtp_short_login']); - $oDomain->bOutAuth = !empty($aDomain['smtp_auth']); - $oDomain->bOutSetSender = !empty($aDomain['smtp_set_sender']); - $oDomain->bOutUsePhpMail = !empty($aDomain['smtp_php_mail']); + $oDomain->SMTP->host = \SnappyMail\IDN::toUtf8($aDomain['smtp_host']); + $oDomain->SMTP->port = (int) ($aDomain['smtp_port'] ?? 25); + $oDomain->SMTP->type = self::StrConnectionSecurityTypeToCons($aDomain['smtp_secure'] ?? ''); + $oDomain->SMTP->shortLogin = !empty($aDomain['smtp_short_login']); + $oDomain->SMTP->useAuth = !empty($aDomain['smtp_auth']); + $oDomain->SMTP->setSender = !empty($aDomain['smtp_set_sender']); + $oDomain->SMTP->usePhpMail = !empty($aDomain['smtp_php_mail']); - $oDomain->sWhiteList = (string) ($aDomain['white_list'] ?? ''); + $oDomain->whiteList = \trim($aDomain['white_list'] ?? ''); + + $oDomain->Normalize(); } - return $oDomain; } - private function encodeIniString(string $sStr) : string - { - return str_replace('"', '\\"', $sStr); - } - public function Normalize() { - $this->sIncHost = \trim($this->sIncHost); - $this->sSieveHost = \trim($this->sSieveHost); - $this->sOutHost = \trim($this->sOutHost); - $this->sWhiteList = \trim($this->sWhiteList); - - if ($this->iIncPort <= 0) - { - $this->iIncPort = 143; - } - - if ($this->iSievePort <= 0) - { - $this->iSievePort = 4190; - } - - if ($this->iOutPort <= 0) - { - $this->iOutPort = 25; - } - } - - public function ToIniString() : string - { - $this->Normalize(); - return \implode("\n", array( - 'imap_host = "'.$this->encodeIniString($this->sIncHost).'"', - 'imap_port = '.$this->iIncPort, - 'imap_secure = "'.self::ConstConnectionSecurityTypeToStr($this->iIncSecure).'"', - 'imap_short_login = '.($this->bIncShortLogin ? 'On' : 'Off'), - 'sieve_use = '.($this->bUseSieve ? 'On' : 'Off'), - 'sieve_host = "'.$this->encodeIniString($this->sSieveHost).'"', - 'sieve_port = '.$this->iSievePort, - 'sieve_secure = "'.self::ConstConnectionSecurityTypeToStr($this->iSieveSecure).'"', - 'smtp_host = "'.$this->encodeIniString($this->sOutHost).'"', - 'smtp_port = '.$this->iOutPort, - 'smtp_secure = "'.self::ConstConnectionSecurityTypeToStr($this->iOutSecure).'"', - 'smtp_short_login = '.($this->bOutShortLogin ? 'On' : 'Off'), - 'smtp_auth = '.($this->bOutAuth ? 'On' : 'Off'), - 'smtp_set_sender = '.($this->bOutSetSender ? 'On' : 'Off'), - 'smtp_php_mail = '.($this->bOutUsePhpMail ? 'On' : 'Off'), - 'white_list = "'.$this->encodeIniString($this->sWhiteList).'"' - )); + $this->IMAP->host = \trim($this->IMAP->host); + $this->Sieve->host = \trim($this->Sieve->host); + $this->SMTP->host = \trim($this->SMTP->host); + $this->whiteList = \trim($this->whiteList); } + /** + * Use by old fromIniArray() + */ public static function StrConnectionSecurityTypeToCons(string $sType) : int { $iSecurityType = ConnectionSecurityType::NONE; - switch (strtoupper($sType)) + switch (\strtoupper($sType)) { case 'SSL': $iSecurityType = ConnectionSecurityType::SSL; @@ -202,22 +90,6 @@ class Domain implements \JsonSerializable return $iSecurityType; } - public static function ConstConnectionSecurityTypeToStr(int $iSecurityType) : string - { - $sType = 'None'; - switch ($iSecurityType) - { - case ConnectionSecurityType::SSL: - $sType = 'SSL'; - break; - case ConnectionSecurityType::STARTTLS: - $sType = 'TLS'; - break; - } - - return $sType; - } - /** * deprecated */ @@ -228,87 +100,89 @@ class Domain implements \JsonSerializable bool $bOutAuth, bool $bOutSetSender, bool $bOutUsePhpMail, string $sWhiteList = '') : self { - $this->sIncHost = $sIncHost; - $this->iIncPort = $iIncPort; - $this->iIncSecure = $iIncSecure; - $this->bIncShortLogin = $bIncShortLogin; + $this->IMAP->host = $sIncHost; + $this->IMAP->port = $iIncPort; + $this->IMAP->type = $iIncSecure; + $this->IMAP->shortLogin = $bIncShortLogin; - $this->bUseSieve = $bUseSieve; - $this->sSieveHost = $sSieveHost; - $this->iSievePort = $iSievePort; - $this->iSieveSecure = $iSieveSecure; + $this->SMTP->host = $sOutHost; + $this->SMTP->port = $iOutPort; + $this->SMTP->type = $iOutSecure; + $this->SMTP->shortLogin = $bOutShortLogin; + $this->SMTP->useAuth = $bOutAuth; + $this->SMTP->setSender = $bOutSetSender; + $this->SMTP->usePhpMail = $bOutUsePhpMail; - $this->sOutHost = $sOutHost; - $this->iOutPort = $iOutPort; - $this->iOutSecure = $iOutSecure; - $this->bOutShortLogin = $bOutShortLogin; - $this->bOutAuth = $bOutAuth; - $this->bOutSetSender = $bOutSetSender; - $this->bOutUsePhpMail = $bOutUsePhpMail; + $this->Sieve->enabled = $bUseSieve; + $this->Sieve->host = $sSieveHost; + $this->Sieve->port = $iSievePort; + $this->Sieve->type = $iSieveSecure; - $this->sWhiteList = \trim($sWhiteList); + $this->whiteList = \trim($sWhiteList); + + $this->Normalize(); return $this; } public function Name() : string { - return $this->sName; + return $this->Name; } public function IncHost() : string { - return $this->sIncHost; + return $this->IMAP->host; } public function IncPort() : int { - return $this->iIncPort; + return $this->IMAP->port; } public function IncShortLogin() : bool { - return $this->bIncShortLogin; + return $this->IMAP->shortLogin; } public function UseSieve() : bool { - return $this->bUseSieve; + return $this->Sieve->enabled; } public function OutHost() : string { - return $this->sOutHost; + return $this->SMTP->host; } public function OutPort() : int { - return $this->iOutPort; + return $this->SMTP->port; } public function OutShortLogin() : bool { - return $this->bOutShortLogin; + return $this->SMTP->shortLogin; } public function OutSetSender() : bool { - return $this->bOutSetSender; + return $this->SMTP->setSender; } public function OutUsePhpMail() : bool { - return $this->bOutUsePhpMail; + return $this->SMTP->usePhpMail; } public function SetAliasName(string $sAliasName) : void { - $this->sAliasName = $sAliasName; + $this->aliasName = $sAliasName; } public function ValidateWhiteList(string $sEmail, string $sLogin = '') : bool { - $sW = \trim($this->sWhiteList); + $sW = \trim($this->whiteList); if (\strlen($sW)) { $sEmail = \MailSo\Base\Utils::IdnToUtf8($sEmail, true); @@ -324,39 +198,19 @@ class Domain implements \JsonSerializable return true; } - public function ImapSettings() : array + public function ImapSettings() : \MailSo\Imap\Settings { - return array( - 'UseConnect' => true, - 'UseAuth' => true, - 'Host' => $this->sIncHost, - 'Port' => $this->iIncPort, - 'Secure' => $this->iIncSecure, - ); + return $this->IMAP; } - public function SmtpSettings() : array + public function SmtpSettings() : \MailSo\Smtp\Settings { - return array( - 'UseConnect' => !$this->bOutUsePhpMail, - 'UseAuth' => $this->bOutAuth, - 'Host' => $this->sOutHost, - 'Port' => $this->iOutPort, - 'Secure' => $this->iOutSecure, - 'Ehlo' => \MailSo\Smtp\SmtpClient::EhloHelper(), - 'UsePhpMail' => $this->bOutUsePhpMail - ); + return $this->SMTP; } - public function SieveSettings() : array + public function SieveSettings() : \MailSo\Sieve\Settings { - return array( - 'UseConnect' => true, - 'UseAuth' => true, - 'Host' => $this->sSieveHost, - 'Port' => $this->iSievePort, - 'Secure' => $this->iSieveSecure - ); + return $this->Sieve; } /** @@ -364,30 +218,40 @@ class Domain implements \JsonSerializable */ public static function fromArray(string $sName, array $aDomain) : ?self { - $oDomain = null; - if (\strlen($sName) && \strlen($aDomain['imapHost'])) { - $oDomain = new self($sName); - - $oDomain->sIncHost = \SnappyMail\IDN::toAscii($aDomain['imapHost']); - $oDomain->iIncPort = (int) $aDomain['imapPort']; - $oDomain->iIncSecure = (int) $aDomain['imapSecure']; - $oDomain->bIncShortLogin = !empty($aDomain['imapShortLogin']); - - $oDomain->bUseSieve = !empty($aDomain['useSieve']); - $oDomain->sSieveHost = \SnappyMail\IDN::toAscii($aDomain['sieveHost']); - $oDomain->iSievePort = (int) $aDomain['sievePort']; - $oDomain->iSieveSecure = (int) $aDomain['sieveSecure']; - - $oDomain->sOutHost = \SnappyMail\IDN::toAscii($aDomain['smtpHost']); - $oDomain->iOutPort = (int) $aDomain['smtpPort']; - $oDomain->iOutSecure = (int) $aDomain['smtpSecure']; - $oDomain->bOutShortLogin = !empty($aDomain['smtpShortLogin']); - $oDomain->bOutAuth = !empty($aDomain['smtpAuth']); - $oDomain->bOutSetSender = !empty($aDomain['smtpSetSender']); - $oDomain->bOutUsePhpMail = !empty($aDomain['smtpPhpMail']); - - $oDomain->sWhiteList = (string) $aDomain['whiteList']; + if (!\strlen($sName)) { + return null; } + $oDomain = new self($sName); + if (!empty($aDomain['IMAP'])) { + $oDomain->IMAP = \MailSo\Imap\Settings::fromArray($aDomain['IMAP']); + $oDomain->SMTP = \MailSo\Smtp\Settings::fromArray($aDomain['SMTP']); + $oDomain->Sieve = \MailSo\Sieve\Settings::fromArray($aDomain['Sieve']); + $oDomain->whiteList = (string) $aDomain['whiteList']; + } else if (\strlen($aDomain['imapHost'])) { + $oDomain->IMAP->host = $aDomain['imapHost']; + $oDomain->IMAP->port = (int) $aDomain['imapPort']; + $oDomain->IMAP->type = (int) $aDomain['imapSecure']; + $oDomain->IMAP->shortLogin = !empty($aDomain['imapShortLogin']); + + $oDomain->Sieve->enabled = !empty($aDomain['useSieve']); + $oDomain->Sieve->host = $aDomain['sieveHost']; + $oDomain->Sieve->port = (int) $aDomain['sievePort']; + $oDomain->Sieve->type = (int) $aDomain['sieveSecure']; + + $oDomain->SMTP->host = $aDomain['smtpHost']; + $oDomain->SMTP->port = (int) $aDomain['smtpPort']; + $oDomain->SMTP->type = (int) $aDomain['smtpSecure']; + $oDomain->SMTP->shortLogin = !empty($aDomain['smtpShortLogin']); + $oDomain->SMTP->useAuth = !empty($aDomain['smtpAuth']); + $oDomain->SMTP->setSender = !empty($aDomain['smtpSetSender']); + $oDomain->SMTP->authPlainLine = !empty($aDomain['smtpAuthPlainLine']); + $oDomain->SMTP->usePhpMail = !empty($aDomain['smtpPhpMail']); + + $oDomain->whiteList = (string) $aDomain['whiteList']; + } else { + return null; + } + $oDomain->Normalize(); return $oDomain; } @@ -396,26 +260,14 @@ class Domain implements \JsonSerializable { $aResult = array( // '@Object' => 'Object/Domain', - 'name' => \SnappyMail\IDN::toUtf8($this->sName), - 'imapHost' => \SnappyMail\IDN::toUtf8($this->sIncHost), - 'imapPort' => $this->iIncPort, - 'imapSecure' => $this->iIncSecure, - 'imapShortLogin' => $this->bIncShortLogin, - 'useSieve' => $this->bUseSieve, - 'sieveHost' => \SnappyMail\IDN::toUtf8($this->sSieveHost), - 'sievePort' => $this->iSievePort, - 'sieveSecure' => $this->iSieveSecure, - 'smtpHost' => \SnappyMail\IDN::toUtf8($this->sOutHost), - 'smtpPort' => $this->iOutPort, - 'smtpSecure' => $this->iOutSecure, - 'smtpShortLogin' => $this->bOutShortLogin, - 'smtpAuth' => $this->bOutAuth, - 'smtpSetSender' => $this->bOutSetSender, - 'smtpPhpMail' => $this->bOutUsePhpMail, - 'whiteList' => $this->sWhiteList + 'name' => $this->Name, + 'IMAP' => $this->IMAP, + 'SMTP' => $this->SMTP, + 'Sieve' => $this->Sieve, + 'whiteList' => $this->whiteList ); - if ($this->sAliasName) { - $aResult['aliasName'] = $this->sAliasName; + if ($this->aliasName) { + $aResult['aliasName'] = $this->aliasName; } return $aResult; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php index 9029fd5f6..921d73b6d 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Identity.php @@ -6,50 +6,24 @@ use MailSo\Base\Utils; class Identity implements \JsonSerializable { - /** - * @var string - */ - private $sId; + private string $sId; - /** - * @var string - */ - private $sEmail; + private string $sEmail; - /** - * @var string - */ - private $sName; + private string $sName = ''; - /** - * @var string - */ - private $sReplyTo; + private string $sReplyTo = ''; - /** - * @var string - */ - private $sBcc; + private string $sBcc = ''; - /** - * @var string - */ - private $sSignature; + private string $sSignature = ''; - /** - * @var bool - */ - private $bSignatureInsertBefore; + private bool $bSignatureInsertBefore = false; function __construct(string $sId = '', string $sEmail = '') { $this->sId = $sId; $this->sEmail = $sEmail; - $this->sName = ''; - $this->sReplyTo = ''; - $this->sBcc = ''; - $this->sSignature = ''; - $this->bSignatureInsertBefore = false; } public function Id(bool $bFillOnEmpty = false): string @@ -84,16 +58,6 @@ class Identity implements \JsonSerializable return $this->sBcc; } - public function Signature(): string - { - return $this->sSignature; - } - - public function SignatureInsertBefore(): bool - { - return $this->bSignatureInsertBefore; - } - public function SetId(string $sId): Identity { $this->sId = $sId; @@ -139,8 +103,7 @@ class Identity implements \JsonSerializable $this->sReplyTo = !empty($aData['ReplyTo']) ? $aData['ReplyTo'] : ''; $this->sBcc = !empty($aData['Bcc']) ? $aData['Bcc'] : ''; $this->sSignature = !empty($aData['Signature']) ? $aData['Signature'] : ''; - $this->bSignatureInsertBefore = isset($aData['SignatureInsertBefore']) ? - !empty($aData['SignatureInsertBefore']) : true; + $this->bSignatureInsertBefore = !empty($aData['SignatureInsertBefore']); return true; } @@ -148,16 +111,17 @@ class Identity implements \JsonSerializable return false; } + // Used to store public function ToSimpleJSON(): array { return array( - 'Id' => $this->Id(), - 'Email' => $this->Email(), - 'Name' => $this->Name(), - 'ReplyTo' => $this->ReplyTo(), - 'Bcc' => $this->Bcc(), - 'Signature' => $this->Signature(), - 'SignatureInsertBefore' => $this->SignatureInsertBefore() + 'Id' => $this->sId, + 'Email' => $this->sEmail, + 'Name' => $this->sName, + 'ReplyTo' => $this->sReplyTo, + 'Bcc' => $this->sBcc, + 'Signature' => $this->sSignature, + 'SignatureInsertBefore' => $this->bSignatureInsertBefore ); } @@ -165,13 +129,14 @@ class Identity implements \JsonSerializable public function jsonSerialize() { return array( - 'Id' => $this->Id(), - 'Email' => Utils::IdnToUtf8($this->Email()), - 'Name' => $this->Name(), - 'ReplyTo' => $this->ReplyTo(), - 'Bcc' => $this->Bcc(), - 'Signature' => $this->Signature(), - 'SignatureInsertBefore' => $this->SignatureInsertBefore() + '@Object' => 'Object/Identity', + 'id' => $this->sId, + 'email' => Utils::IdnToUtf8($this->sEmail), + 'name' => $this->sName, + 'replyTo' => $this->sReplyTo, + 'bcc' => $this->sBcc, + 'signature' => $this->sSignature, + 'signatureInsertBefore' => $this->bSignatureInsertBefore ); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php index cd74f9672..6e376d4b9 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Notifications.php @@ -70,8 +70,7 @@ class Notifications static public function GetNotificationsMessage(int $iCode, ?\Throwable $oPrevious = null) : string { - if (self::ClientViewError === $iCode && $oPrevious) - { + if (self::ClientViewError === $iCode && $oPrevious) { return $oPrevious->getMessage(); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php similarity index 67% rename from snappymail/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php rename to snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php index c29fb84f5..08a57f779 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Common/PdoAbstract.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Base.php @@ -1,48 +1,25 @@ oLogger = $oLogger; - } - - protected function getPdoAccessData() : array - { - return array('', '', '', ''); - } + abstract protected function getPdoSettings() : \RainLoop\Pdo\Settings; public function sqliteNoCaseCollationHelper(string $sStr1, string $sStr2) : int { @@ -63,80 +40,70 @@ abstract class PdoAbstract */ protected function getPDO() : \PDO { - if ($this->oPDO) - { + if ($this->oPDO) { return $this->oPDO; } - if (!\class_exists('PDO')) - { + if (!\class_exists('PDO')) { throw new \Exception('Class PDO does not exist'); } - $sType = $sDsn = $sDbLogin = $sDbPassword = ''; - list($sType, $sDsn, $sDbLogin, $sDbPassword) = $this->getPdoAccessData(); + $oSettings = $this->getPdoSettings(); - if (!\in_array($sType, static::getAvailableDrivers())) - { + if (!\in_array($oSettings->driver, static::getAvailableDrivers())) { throw new \Exception('Unknown PDO SQL connection type'); } - if (empty($sDsn)) - { + if (empty($oSettings->dsn)) { throw new \Exception('Empty PDO DSN configuration'); } - $this->sDbType = $sType; + $this->sDbType = $oSettings->driver; - $oPdo = false; - try - { -// $bCaseFunc = false; - $oPdo = new \PDO($sDsn, $sDbLogin, $sDbPassword); - if ($oPdo) - { - $sPdoType = $oPdo->getAttribute(\PDO::ATTR_DRIVER_NAME); - $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - if ('mysql' === $sType && 'mysql' === $sPdoType) - { - $oPdo->exec('SET NAMES utf8mb4 COLLATE utf8mb4_general_ci'); - } -// else if ('sqlite' === $sType && 'sqlite' === $sPdoType && $this->bSqliteCollate) -// { -// if (\method_exists($oPdo, 'sqliteCreateCollation')) -// { -// $oPdo->sqliteCreateCollation('SQLITE_NOCASE_UTF8', array($this, 'sqliteNoCaseCollationHelper')); -// $bCaseFunc = true; -// } -// } -// -// $this->oLogger->Write('PDO:'.$sPdoType.($bCaseFunc ? '/SQLITE_NOCASE_UTF8' : '')); + $options = []; + if ('mysql' === $oSettings->driver) { + if ($oSettings->sslCa) { + $options[\PDO::MYSQL_ATTR_SSL_CA] = $oSettings->sslCa; } - } - catch (\Throwable $oException) - { - throw $oException; + // PHP 8.0 https://github.com/the-djmaze/snappymail/issues/1205 + if (\defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) { + $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $oSettings->sslVerify; + } + if ($oSettings->sslCiphers) { + $options[\PDO::MYSQL_ATTR_SSL_CIPHER] = $oSettings->sslCiphers; + } +/* + $options[\PDO::MYSQL_ATTR_SSL_CAPATH] = ''; + // mutual (two-way) authentication + $options[\PDO::MYSQL_ATTR_SSL_KEY] = ''; + $options[\PDO::MYSQL_ATTR_SSL_CERT] = ''; +*/ } - if ($oPdo) - { - $this->oPDO = $oPdo; - } - else - { - throw new \Exception('PDO = false'); - } + $oPdo = new \PDO($oSettings->dsn, $oSettings->user, $oSettings->password, $options); + $sPdoType = $oPdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + $oPdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); +// $bCaseFunc = false; + if ('mysql' === $oSettings->driver && 'mysql' === $sPdoType) { + $oPdo->exec('SET NAMES utf8mb4 COLLATE utf8mb4_general_ci'); + } +// else if ('sqlite' === $oSettings->driver && 'sqlite' === $sPdoType && $this->bSqliteCollate) { +// if (\method_exists($oPdo, 'sqliteCreateCollation')) { +// $oPdo->sqliteCreateCollation('SQLITE_NOCASE_UTF8', array($this, 'sqliteNoCaseCollationHelper')); +// $bCaseFunc = true; +// } +// } +// $this->logWrite('PDO:'.$sPdoType.($bCaseFunc ? '/SQLITE_NOCASE_UTF8' : '')); + + $this->oPDO = $oPdo; return $oPdo; } protected function lastInsertId(?string $sTabelName = null, ?string $sColumnName = null) : string { $mName = null; - if ('pgsql' === $this->sDbType && - null !== $sTabelName && $sColumnName !== null) - { + if ('pgsql' === $this->sDbType && null !== $sTabelName && $sColumnName !== null) { $mName = \strtolower($sTabelName.'_'.$sColumnName.'_seq'); } @@ -160,8 +127,7 @@ abstract class PdoAbstract protected function prepareAndExecute(string $sSql, array $aParams = array(), bool $bMultiplyParams = false, bool $bLogParams = false) : ?\PDOStatement { - if ($this->bExplain && !$bMultiplyParams) - { + if ($this->bExplain && !$bMultiplyParams) { $this->prepareAndExplain($sSql, $aParams); } @@ -169,27 +135,19 @@ abstract class PdoAbstract $this->writeLog($sSql); $oStmt = $this->getPDO()->prepare($sSql); - if ($oStmt) - { + if ($oStmt) { $aLogs = array(); $aRootParams = $bMultiplyParams ? $aParams : array($aParams); - foreach ($aRootParams as $aSubParams) - { - foreach ($aSubParams as $sName => $aValue) - { - if ($bLogParams) - { + foreach ($aRootParams as $aSubParams) { + foreach ($aSubParams as $sName => $aValue) { + if ($bLogParams) { $aLogs[$sName] = $aValue[0]; } - $oStmt->bindValue($sName, $aValue[0], $aValue[1]); } - $mResult = $oStmt->execute() && !$bMultiplyParams ? $oStmt : null; } - - if ($bLogParams && $aLogs) - { + if ($bLogParams && $aLogs) { $this->writeLog('Params: '.\json_encode($aLogs, JSON_UNESCAPED_UNICODE)); } } @@ -200,15 +158,12 @@ abstract class PdoAbstract protected function prepareAndExplain(string $sSql, array $aParams = array()) { $mResult = null; - if (0 === strpos($sSql, 'SELECT ')) - { + if (0 === \strpos($sSql, 'SELECT ')) { $sSql = 'EXPLAIN '.$sSql; $this->writeLog($sSql); $oStmt = $this->getPDO()->prepare($sSql); - if ($oStmt) - { - foreach ($aParams as $sName => $aValue) - { + if ($oStmt) { + foreach ($aParams as $sName => $aValue) { $oStmt->bindValue($sName, $aValue[0], $aValue[1]); } @@ -216,8 +171,7 @@ abstract class PdoAbstract } } - if ($mResult) - { + if ($mResult) { $aFetch = $mResult->fetchAll(\PDO::FETCH_ASSOC); $this->oLogger->WriteDump($aFetch); @@ -231,18 +185,12 @@ abstract class PdoAbstract */ protected function writeLog($mData) { - if ($this->oLogger) - { - if ($mData instanceof \Throwable) - { + if ($this->oLogger) { + if ($mData instanceof \Throwable) { $this->oLogger->WriteException($mData, \LOG_ERR, 'SQL'); - } - else if (\is_scalar($mData)) - { + } else if (\is_scalar($mData)) { $this->oLogger->Write((string) $mData, \LOG_INFO, 'SQL'); - } - else - { + } else { $this->oLogger->WriteDump($mData, \LOG_INFO, 'SQL'); } } @@ -257,18 +205,15 @@ abstract class PdoAbstract protected function getVersion(string $sName) : ?int { $oPdo = $this->getPDO(); - if ($oPdo) - { + if ($oPdo) { $sQuery = 'SELECT MAX(value_int) FROM rainloop_system WHERE sys_name = ?'; $this->writeLog($sQuery); $oStmt = $oPdo->prepare($sQuery); - if ($oStmt->execute(array($sName.'_version'))) - { + if ($oStmt->execute(array($sName.'_version'))) { $mRow = $oStmt->fetch(\PDO::FETCH_NUM); - if ($mRow && isset($mRow[0])) - { + if ($mRow && isset($mRow[0])) { return (int) $mRow[0]; } @@ -283,21 +228,18 @@ abstract class PdoAbstract { $bResult = false; $oPdo = $this->getPDO(); - if ($oPdo) - { + if ($oPdo) { $sQuery = 'DELETE FROM rainloop_system WHERE sys_name = ? AND value_int <= ?;'; $this->writeLog($sQuery); $oStmt = $oPdo->prepare($sQuery); $bResult = !!$oStmt->execute(array($sName.'_version', $iVersion)); - if ($bResult) - { + if ($bResult) { $sQuery = 'INSERT INTO rainloop_system (sys_name, value_int) VALUES (?, ?);'; $this->writeLog($sQuery); $oStmt = $oPdo->prepare($sQuery); - if ($oStmt) - { + if ($oStmt) { $bResult = !!$oStmt->execute(array($sName.'_version', $iVersion)); } } @@ -314,8 +256,7 @@ abstract class PdoAbstract $bResult = true; $oPdo = $this->getPDO(); - if ($oPdo) - { + if ($oPdo) { $aQ = array(); switch ($this->sDbType) { @@ -361,24 +302,17 @@ rl_email text NOT NULL DEFAULT \'\' break; } - if (\count($aQ)) - { + if (\count($aQ)) { try { - foreach ($aQ as $sQuery) - { - if ($bResult) - { - $this->writeLog($sQuery); - $bResult = false !== $oPdo->exec($sQuery); - if (!$bResult) - { - $this->writeLog('Result=false'); - } - else - { - $this->writeLog('Result=true'); - } + foreach ($aQ as $sQuery) { + $this->writeLog($sQuery); + $bResult = false !== $oPdo->exec($sQuery); + if (!$bResult) { + $this->writeLog('Result=false'); + break; + } else { + $this->writeLog('Result=true'); } } } @@ -402,12 +336,10 @@ rl_email text NOT NULL DEFAULT \'\' } catch (\PDOException $oException) { - $this->writeLog($oException); - +// $this->writeLog($oException); try { $this->initSystemTables(); - $iFromVersion = $this->getVersion($sName); } catch (\PDOException $oSubException) @@ -418,33 +350,26 @@ rl_email text NOT NULL DEFAULT \'\' } $bResult = false; - if (\is_int($iFromVersion) && 0 <= $iFromVersion) - { + if (\is_int($iFromVersion) && 0 <= $iFromVersion) { $oPdo = false; - foreach ($aData as $iVersion => $aQuery) - { - if (0 === \count($aQuery)) - { + foreach ($aData as $iVersion => $aQuery) { + if (0 === \count($aQuery)) { continue; } - if (!$oPdo) - { + if (!$oPdo) { $oPdo = $this->getPDO(); $bResult = true; } - if ($iFromVersion < $iVersion && $oPdo) - { + if ($iFromVersion < $iVersion && $oPdo) { try { - foreach ($aQuery as $sQuery) - { + foreach ($aQuery as $sQuery) { $this->writeLog($sQuery); $bExec = $oPdo->exec($sQuery); - if (false === $bExec) - { + if (false === $bExec) { $this->writeLog('Result: false'); $bResult = false; @@ -458,8 +383,7 @@ rl_email text NOT NULL DEFAULT \'\' throw $oException; } - if (!$bResult) - { + if (!$bResult) { break; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Settings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Settings.php new file mode 100644 index 000000000..ecddde9f6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Pdo/Settings.php @@ -0,0 +1,20 @@ +bLangs = $bLangs; } @@ -113,8 +100,7 @@ abstract class AbstractPlugin final public function ConfigMap(bool $flatten = false) : array { - if (null === $this->aConfigMap) - { + if (null === $this->aConfigMap) { $this->aConfigMap = $this->configMapping(); } @@ -163,8 +149,7 @@ abstract class AbstractPlugin final public function SetPluginConfig(\RainLoop\Config\Plugin $oPluginConfig) : self { $this->oPluginConfig = $oPluginConfig; - if ($oPluginConfig->IsInited() && !$oPluginConfig->Load()) - { + if ($oPluginConfig->IsInited() && !$oPluginConfig->Load()) { $oPluginConfig->Save(); } return $this; @@ -172,8 +157,7 @@ abstract class AbstractPlugin final protected function addHook(string $sHookName, string $sFunctionName) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddHook($sHookName, array($this, $sFunctionName)); } @@ -182,8 +166,7 @@ abstract class AbstractPlugin final protected function addCss(string $sFile, bool $bAdminScope = false) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddCss($this->sPath.'/'.$sFile, $bAdminScope); } @@ -192,8 +175,7 @@ abstract class AbstractPlugin final protected function addJs(string $sFile, bool $bAdminScope = false) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddJs($this->sPath.'/'.$sFile, $bAdminScope); } @@ -202,8 +184,7 @@ abstract class AbstractPlugin final protected function addTemplate(string $sFile, bool $bAdminScope = false) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddTemplate($this->sPath.'/'.$sFile, $bAdminScope); } @@ -212,8 +193,7 @@ abstract class AbstractPlugin final protected function replaceTemplate(string $sFile, bool $bAdminScope = false) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddTemplate($this->sPath.'/'.$sFile, $bAdminScope); } @@ -222,8 +202,7 @@ abstract class AbstractPlugin final protected function addPartHook(string $sActionName, string $sFunctionName) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddAdditionalPartAction($sActionName, array($this, $sFunctionName)); } @@ -232,8 +211,7 @@ abstract class AbstractPlugin final protected function addJsonHook(string $sActionName, string $sFunctionName) : self { - if ($this->oPluginManager) - { + if ($this->oPluginManager) { $this->oPluginManager->AddAdditionalJsonAction($sActionName, array($this, $sFunctionName)); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Manager.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Manager.php index 9dc10e72c..ef7ebcb79 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Manager.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Manager.php @@ -4,12 +4,14 @@ namespace RainLoop\Plugins; class Manager { + use \MailSo\Log\Inherit; + /** * @var \RainLoop\Actions */ private $oActions; - private + private array $aHooks = array(), $aCss = array([], []), $aJs = array([], []), @@ -19,19 +21,10 @@ class Manager $aAdditionalJson = array(), $aPlugins = array(); - /** - * @var bool - */ - private $bIsEnabled; - - /** - * @var \MailSo\Log\Logger - */ - private $oLogger; + private bool $bIsEnabled; public function __construct(\RainLoop\Actions $oActions) { - $this->oLogger = null; $this->oActions = $oActions; $oConfig = $oActions->Config(); @@ -41,15 +34,11 @@ class Manager if (\strlen($sList)) { $aList = \SnappyMail\Repository::getEnabledPackagesNames(); foreach ($aList as $i => $sName) { - $oPlugin = $this->CreatePluginByName($sName); - if ($oPlugin) { - $oPlugin->Init(); - $this->aPlugins[] = $oPlugin; - } else { + if (!$this->loadPlugin($sName)) { unset($aList[$i]); } } - $aList = \implode(',', \array_unique($aList)); + $aList = \implode(',', \array_keys($this->aPlugins)); if ($sList != $aList) { $oConfig->Set('plugins', 'enabled_list', $aList); $oConfig->Save(); @@ -58,6 +47,18 @@ class Manager } } + public function loadPlugin(string $sName) : bool + { + if (!isset($this->aPlugins[$sName])) { + $oPlugin = $this->CreatePluginByName($sName); + if ($oPlugin) { + $oPlugin->Init(); + $this->aPlugins[$sName] = $oPlugin; + } + } + return isset($this->aPlugins[$sName]); + } + protected static function getPluginPath(string $sName) : ?string { $sPath = APP_PLUGINS_PATH.$sName; @@ -96,10 +97,8 @@ class Manager $aList = array(); $aGlob = \glob(APP_PLUGINS_PATH.'*'); - if (\is_array($aGlob)) - { - foreach ($aGlob as $sPathName) - { + if (\is_array($aGlob)) { + foreach ($aGlob as $sPathName) { if (\is_dir($sPathName)) { $sName = \basename($sPathName); } else if ('.phar' === \substr($sPathName, -5)) { @@ -117,11 +116,8 @@ class Manager ); } } - } - else - { - $this->oActions->Logger()->Write('Cannot get installed plugins from '.APP_PLUGINS_PATH, - \LOG_ERR); + } else { + $this->oActions->logWrite('Cannot get installed plugins from '.APP_PLUGINS_PATH, \LOG_ERR); } return $aList; @@ -161,9 +157,13 @@ class Manager public function Hash() : string { - return \md5(\array_reduce($this->aPlugins, function($sResult, $oPlugin){ - return $sResult . "|{$oPlugin->Hash()}"; - }, APP_VERSION)); + return \md5( + \array_reduce($this->aPlugins, function($sResult, $oPlugin){ + return $sResult . "|{$oPlugin->Hash()}"; + }, APP_VERSION) + .implode('',$this->aJs[1]).implode('',$this->aJs[0]) + .implode('',$this->aCss[1]).implode('',$this->aCss[0]) + ); } public function HaveJs(bool $bAdminScope = false) : bool @@ -171,11 +171,17 @@ class Manager return $this->bIsEnabled && \count($this->aJs[$bAdminScope ? 1 : 0]); } - public function CompileCss(bool $bAdminScope, bool &$bLess) : string + public function CompileCss(bool $bAdminScope, bool &$bLess, bool $bMinified) : string { $aResult = array(); if ($this->bIsEnabled) { foreach ($this->aCss[$bAdminScope ? 1 : 0] as $sFile) { + if ($bMinified) { + $sMinFile = \str_replace('.css', '.min.css', $sFile); + if (\is_readable($sMinFile)) { + $sFile = $sMinFile; + } + } if (\is_readable($sFile)) { $aResult[] = \file_get_contents($sFile); $bLess = $bLess || \str_ends_with($sFile, '.less'); @@ -185,15 +191,18 @@ class Manager return \implode("\n", $aResult); } - public function CompileJs(bool $bAdminScope = false) : string + public function CompileJs(bool $bAdminScope = false, bool $bMinified = false) : string { $aResult = array(); - if ($this->bIsEnabled) - { - foreach ($this->aJs[$bAdminScope ? 1 : 0] as $sFile) - { - if (\is_readable($sFile)) - { + if ($this->bIsEnabled) { + foreach ($this->aJs[$bAdminScope ? 1 : 0] as $sFile) { + if ($bMinified) { + $sMinFile = \str_replace('.js', '.min.js', $sFile); + if (\is_readable($sMinFile)) { + $sFile = $sMinFile; + } + } + if (\is_readable($sFile)) { $aResult[] = \file_get_contents($sFile); } } @@ -204,13 +213,10 @@ class Manager public function CompileTemplate(array &$aList, bool $bAdminScope = false) : void { - if ($this->bIsEnabled) - { + if ($this->bIsEnabled) { $aTemplates = $bAdminScope ? $this->aAdminTemplates : $this->aTemplates; - foreach ($aTemplates as $sFile) - { - if (\is_readable($sFile)) - { + foreach ($aTemplates as $sFile) { + if (\is_readable($sFile)) { $sTemplateName = \substr(\basename($sFile), 0, -5); $aList[$sTemplateName] = $sFile; } @@ -220,21 +226,15 @@ class Manager public function InitAppData(bool $bAdmin, array &$aAppData, ?\RainLoop\Model\Account $oAccount = null) : self { - if ($this->bIsEnabled && isset($aAppData['Plugins']) && \is_array($aAppData['Plugins'])) - { + if ($this->bIsEnabled && isset($aAppData['Plugins']) && \is_array($aAppData['Plugins'])) { $bAuth = !empty($aAppData['Auth']); - foreach ($this->aPlugins as $oPlugin) - { - if ($oPlugin) - { + foreach ($this->aPlugins as $oPlugin) { + if ($oPlugin) { $aConfig = array(); $aMap = $oPlugin->ConfigMap(true); - if (\is_array($aMap)) - { - foreach ($aMap as /* @var $oPluginProperty \RainLoop\Plugins\Property */ $oPluginProperty) - { - if ($oPluginProperty && $oPluginProperty->AllowedInJs()) - { + if (\is_array($aMap)) { + foreach ($aMap as /* @var $oPluginProperty \RainLoop\Plugins\Property */ $oPluginProperty) { + if ($oPluginProperty && $oPluginProperty->AllowedInJs()) { $aConfig[$oPluginProperty->Name()] = $oPlugin->Config()->Get('plugin', $oPluginProperty->Name(), @@ -245,8 +245,7 @@ class Manager $oPlugin->FilterAppDataPluginSection($bAdmin, $bAuth, $aConfig); - if (\count($aConfig)) - { + if (\count($aConfig)) { $aAppData['Plugins'][$oPlugin->Name()] = $aConfig; } } @@ -263,16 +262,13 @@ class Manager */ public function AddHook(string $sHookName, $mCallbak) : self { - if ($this->bIsEnabled && \is_callable($mCallbak)) - { - if (!isset($this->aHooks[$sHookName])) - { + if ($this->bIsEnabled && \is_callable($mCallbak)) { + $sHookName = \strtolower($sHookName); + if (!isset($this->aHooks[$sHookName])) { $this->aHooks[$sHookName] = array(); } - $this->aHooks[$sHookName][] = $mCallbak; } - return $this; } @@ -295,14 +291,10 @@ class Manager public function AddTemplate(string $sFile, bool $bAdminScope = false) : self { - if ($this->bIsEnabled) - { - if ($bAdminScope) - { + if ($this->bIsEnabled) { + if ($bAdminScope) { $this->aAdminTemplates[$sFile] = $sFile; - } - else - { + } else { $this->aTemplates[$sFile] = $sFile; } } @@ -312,22 +304,17 @@ class Manager public function RunHook(string $sHookName, array $aArg = array(), bool $bLogHook = true) : self { - if ($this->bIsEnabled) - { - if (isset($this->aHooks[$sHookName])) - { - if ($bLogHook) - { + if ($this->bIsEnabled) { + $sHookName = \strtolower($sHookName); + if (isset($this->aHooks[$sHookName])) { + if ($bLogHook) { $this->WriteLog('Hook: '.$sHookName, \LOG_INFO); } - - foreach ($this->aHooks[$sHookName] as $mCallback) - { + foreach ($this->aHooks[$sHookName] as $mCallback) { $mCallback(...$aArg); } } } - return $this; } @@ -336,11 +323,9 @@ class Manager */ public function AddAdditionalPartAction(string $sActionName, $mCallbak) : self { - if ($this->bIsEnabled && \is_callable($mCallbak)) - { + if ($this->bIsEnabled && \is_callable($mCallbak)) { $sActionName = \strtolower($sActionName); - if (!isset($this->aAdditionalParts[$sActionName])) - { + if (!isset($this->aAdditionalParts[$sActionName])) { $this->aAdditionalParts[$sActionName] = array(); } @@ -353,18 +338,11 @@ class Manager public function RunAdditionalPart(string $sActionName, array $aParts = array()) : bool { $bResult = false; - if ($this->bIsEnabled) - { + if ($this->bIsEnabled) { $sActionName = \strtolower($sActionName); - if (isset($this->aAdditionalParts[$sActionName])) - { - foreach ($this->aAdditionalParts[$sActionName] as $mCallbak) - { - $bCallResult = $mCallbak(...$aParts); - if ($bCallResult && !$bResult) - { - $bResult = true; - } + if (isset($this->aAdditionalParts[$sActionName])) { + foreach ($this->aAdditionalParts[$sActionName] as $mCallbak) { + $bResult = !!$mCallbak(...$aParts) || $bResult; } } } @@ -372,21 +350,12 @@ class Manager return $bResult; } - /** - * @param mixed $mCallback - */ - public function AddAdditionalJsonAction(string $sActionName, $mCallback) : self + public function AddAdditionalJsonAction(string $sActionName, callable $mCallback) : self { - if ($this->bIsEnabled && \is_callable($mCallback) && \strlen($sActionName)) - { - $sActionName = 'DoPlugin'.$sActionName; - - if (!isset($this->aAdditionalJson[$sActionName])) - { - $this->aAdditionalJson[$sActionName] = $mCallback; - } + $sActionName = "DoPlugin{$sActionName}"; + if ($this->bIsEnabled && \strlen($sActionName) && !isset($this->aAdditionalJson[$sActionName])) { + $this->aAdditionalJson[$sActionName] = $mCallback; } - return $this; } @@ -400,15 +369,7 @@ class Manager */ public function RunAdditionalJson(string $sActionName) { - if ($this->bIsEnabled) - { - if (isset($this->aAdditionalJson[$sActionName])) - { - return $this->aAdditionalJson[$sActionName](); - } - } - - return false; + return $this->HasAdditionalJson($sActionName) ? $this->aAdditionalJson[$sActionName]() : false; } /** @@ -416,20 +377,17 @@ class Manager */ public function JsonResponseHelper(string $sFunctionName, $mData) : array { - return $this->oActions->DefaultResponse($sFunctionName, $mData); + return $this->oActions->DefaultResponse($mData, [], $sFunctionName); } public function GetUserPluginSettings(string $sPluginName) : array { $oAccount = $this->oActions->GetAccount(); - if ($oAccount) - { + if ($oAccount) { $oSettings = $this->oActions->SettingsProvider()->Load($oAccount); - if ($oSettings) - { + if ($oSettings) { $aData = $oSettings->GetConf('Plugins', array()); - if (isset($aData[$sPluginName]) && \is_array($aData[$sPluginName])) - { + if (isset($aData[$sPluginName]) && \is_array($aData[$sPluginName])) { return $aData[$sPluginName]; } } @@ -441,25 +399,20 @@ class Manager public function SaveUserPluginSettings(string $sPluginName, array $aSettings) : bool { $oAccount = $this->oActions->GetAccount(); - if ($oAccount) - { + if ($oAccount) { $oSettings = $this->oActions->SettingsProvider()->Load($oAccount); - if ($oSettings) - { + if ($oSettings) { $aData = $oSettings->GetConf('Plugins', array()); - if (!\is_array($aData)) - { + if (!\is_array($aData)) { $aData = array(); } $aPluginSettings = array(); - if (isset($aData[$sPluginName]) && \is_array($aData[$sPluginName])) - { + if (isset($aData[$sPluginName]) && \is_array($aData[$sPluginName])) { $aPluginSettings = $aData[$sPluginName]; } - foreach ($aSettings as $sKey => $mValue) - { + foreach ($aSettings as $sKey => $mValue) { $aPluginSettings[$sKey] = $mValue; } @@ -482,10 +435,10 @@ class Manager $aPLang = []; // First get english - if (\is_file("{$sPath}en.ini")) { - $aPLang = \parse_ini_file("{$sPath}en.ini", true); - } else if (\is_file("{$sPath}en.json")) { + if (\is_file("{$sPath}en.json")) { $aPLang = \json_decode(\file_get_contents("{$sPath}en.json"), true); + } else if (\is_file("{$sPath}en.ini")) { + $aPLang = \parse_ini_file("{$sPath}en.ini", true); } if ($aPLang) { $aLang = \array_replace_recursive($aLang, $aPLang); @@ -494,12 +447,15 @@ class Manager // Now get native if ('en' !== $sLang) { $aPLang = []; - if (\is_file("{$sPath}{$sLang}.ini")) { - $aPLang = \parse_ini_file("{$sPath}{$sLang}.ini", true); - } else if (\is_file($sPath.\strtr($sLang,'-','_').'.ini')) { - $aPLang = \parse_ini_file($sPath.\strtr($sLang,'-','_').'.ini', true); - } else if (\is_file("{$sPath}{$sLang}.json")) { + if (\is_file("{$sPath}{$sLang}.json")) { $aPLang = \json_decode(\file_get_contents("{$sPath}{$sLang}.json"), true); + } else { + if (!\is_file("{$sPath}{$sLang}.ini")) { + $sLang = \strtr($sLang, '-', '_'); + } + if (\is_file("{$sPath}{$sLang}.ini")) { + $aPLang = \parse_ini_file("{$sPath}{$sLang}.ini", true); + } } if ($aPLang) { $aLang = \array_replace_recursive($aLang, $aPLang); @@ -515,31 +471,18 @@ class Manager return $this->bIsEnabled; } - public function Count() : int + public function count() : int { return $this->bIsEnabled ? \count($this->aPlugins) : 0; } - public function SetLogger(\MailSo\Log\Logger $oLogger) : self - { - $this->oLogger = $oLogger; - - return $this; - } - public function WriteLog(string $sDesc, int $iType = \LOG_INFO) : void { - if ($this->oLogger) - { - $this->oLogger->Write($sDesc, $iType, 'PLUGIN'); - } + $this->logWrite($sDesc, $iType, 'PLUGIN'); } public function WriteException(string $sDesc, int $iType = \LOG_INFO) : void { - if ($this->oLogger) - { - $this->oLogger->WriteException($sDesc, $iType, 'PLUGIN'); - } + $this->logException($sDesc, $iType, 'PLUGIN'); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php index 4a223800e..8a6504ff4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/Property.php @@ -2,57 +2,39 @@ namespace RainLoop\Plugins; +use RainLoop\Enumerations\PluginPropertyType; + class Property implements \JsonSerializable { - /** - * @var string - */ - private $sName; + private string $sName; /** * @var mixed */ private $mValue; - /** - * @var string - */ - private $sLabel; + private string $sLabel = ''; - /** - * @var string - */ - private $sDesc; + private string $sDesc = ''; - /** - * @var int - */ - private $iType; + private int $iType = PluginPropertyType::STRING; - /** - * @var bool - */ - private $bAllowedInJs; + private bool $bAllowedInJs = false; /** * @var mixed */ - private $mDefaultValue; + private $mDefaultValue = ''; - /** - * @var string - */ - private $sPlaceholder; + private array $aOptions = []; + + private string $sPlaceholder = ''; + + public bool $encrypted = false; function __construct(string $sName) { $this->sName = $sName; - $this->iType = \RainLoop\Enumerations\PluginPropertyType::STRING; - $this->mDefaultValue = ''; - $this->sLabel = ''; - $this->sDesc = ''; - $this->bAllowedInJs = false; - $this->sPlaceholder = ''; } public static function NewInstance(string $sName) : self @@ -63,7 +45,6 @@ class Property implements \JsonSerializable public function SetType(int $iType) : self { $this->iType = (int) $iType; - return $this; } @@ -72,7 +53,35 @@ class Property implements \JsonSerializable */ public function SetValue($mValue) : void { - $this->mValue = $mValue; + $this->mValue = null; + switch ($this->iType) { + case PluginPropertyType::INT: + $this->mValue = (int) $mValue; + break; + case PluginPropertyType::BOOL: + $this->mValue = !empty($mValue); + break; + case PluginPropertyType::SELECT: + foreach ($this->aOptions as $option) { + if ($mValue == $option['id']) { + $this->mValue = (string) $mValue; + } + } + break; + case PluginPropertyType::SELECTION: + if ($this->aOptions && \in_array($mValue, $this->aOptions)) { + $this->mValue = (string) $mValue; + } + break; + case PluginPropertyType::PASSWORD: + case PluginPropertyType::STRING: + case PluginPropertyType::STRING_TEXT: + case PluginPropertyType::URL: + $this->mValue = (string) $mValue; + break; +// case PluginPropertyType::GROUP: +// throw new \Exception('Not allowed to set group value'); + } } /** @@ -80,36 +89,47 @@ class Property implements \JsonSerializable */ public function SetDefaultValue($mDefaultValue) : self { - $this->mDefaultValue = $mDefaultValue; + if (\is_array($mDefaultValue)) { + $this->aOptions = $mDefaultValue; + } else { + $this->mDefaultValue = $mDefaultValue; + } + return $this; + } + public function SetOptions(array $aOptions) : self + { + $this->aOptions = $aOptions; return $this; } public function SetPlaceholder(string $sPlaceholder) : self { $this->sPlaceholder = $sPlaceholder; - return $this; } public function SetLabel(string $sLabel) : self { $this->sLabel = $sLabel; - return $this; } public function SetDescription(string $sDesc) : self { $this->sDesc = $sDesc; - return $this; } public function SetAllowedInJs(bool $bValue = true) : self { $this->bAllowedInJs = $bValue; + return $this; + } + public function SetEncrypted(bool $bValue = true) : self + { + $this->encrypted = $bValue; return $this; } @@ -146,23 +166,42 @@ class Property implements \JsonSerializable return $this->mDefaultValue; } + public function Options() : array + { + return $this->aOptions; + } + public function Placeholder() : string { return $this->sPlaceholder; } + /** + * @return mixed + */ + public function Value() + { + return $this->mValue; + } + #[\ReturnTypeWillChange] public function jsonSerialize() { + $mValue = $this->mValue; + if ($this->encrypted && $mValue) try { + $mValue = \SnappyMail\Crypt::DecryptFromJSON($mValue, \APP_SALT); + } catch (\Throwable $e) { + } return array( '@Object' => 'Object/PluginProperty', - 'value' => $this->mValue, + 'value' => $mValue, 'placeholder' => $this->sPlaceholder, - 'Name' => $this->sName, - 'Type' => $this->iType, - 'Label' => $this->sLabel, - 'Default' => $this->mDefaultValue, - 'Desc' => $this->sDesc + 'name' => $this->sName, + 'type' => $this->iType, + 'label' => $this->sLabel, + 'default' => $this->mDefaultValue, + 'options' => $this->aOptions, + 'desc' => $this->sDesc ); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php index 1c59b3bf0..f8f2cd26f 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/PropertyCollection.php @@ -20,8 +20,8 @@ class PropertyCollection extends \ArrayObject implements \JsonSerializable { return array( '@Object' => 'Object/PluginProperty', - 'Type' => \RainLoop\Enumerations\PluginPropertyType::GROUP, - 'Label' => $this->sLabel, + 'type' => \RainLoop\Enumerations\PluginPropertyType::GROUP, + 'label' => $this->sLabel, 'config' => $this->getArrayCopy() /* 'config' => [ diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php index 91851693e..f499c712e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AbstractProvider.php @@ -4,16 +4,13 @@ namespace RainLoop\Providers; abstract class AbstractProvider { + use \MailSo\Log\Inherit; + /** * @var \RainLoop\Model\Account */ protected $oAccount; - /** - * @var \MailSo\Log\Logger - */ - protected $oLogger = null; - public function IsActive() : bool { return false; @@ -23,14 +20,4 @@ abstract class AbstractProvider { $this->oAccount = $oAccount; } - - public function SetLogger(?\MailSo\Log\Logger $oLogger) - { - $this->oLogger = $oLogger; - } - - public function Logger() : ?\MailSo\Log\Logger - { - return $this->oLogger; - } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php index d9f0fd987..affaecd99 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook.php @@ -60,6 +60,11 @@ class AddressBook extends AbstractProvider ) : array(); } + public function GetContactByEmail(string $sEmail) : ?AddressBook\Classes\Contact + { + return $this->IsActive() ? $this->oDriver->GetContactByEmail($sEmail) : null; + } + public function GetContactByID($mID, bool $bIsStrID = false) : ?AddressBook\Classes\Contact { return $this->IsActive() ? $this->oDriver->GetContactByID($mID, $bIsStrID) : null; @@ -77,37 +82,4 @@ class AddressBook extends AbstractProvider { return $this->IsActive() ? $this->oDriver->IncFrec($aEmails, $bCreateAuto) : false; } - - public function ImportCsvArray(array $aCsvData) : int - { - $iCount = 0; - if ($this->IsActive()) { - foreach (AddressBook\Utils::CsvArrayToContacts($aCsvData) as $oContact) { - if ($this->ContactSave($oContact)) { - ++$iCount; - } - } - } - return $iCount; - } - - public function ImportVcfFile(string $sVcfData) : int - { - $iCount = 0; - if ($this->IsActive()) { - try - { - foreach (AddressBook\Utils::VcfFileToContacts($sVcfData) as $oContact) { - if ($this->ContactSave($oContact)) { - ++$iCount; - } - } - } - catch (\Throwable $oExc) - { - $this->Logger()->WriteException($oExc); - } - } - return $iCount; - } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php index d0ce8ef54..564a0d48b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/AddressBookInterface.php @@ -20,6 +20,8 @@ interface AddressBookInterface public function GetContacts(int $iOffset = 0, int $iLimit = 20, string $sSearch = '', int &$iResultCount = 0) : array; + public function GetContactByEmail(string $sEmail) : ?Classes\Contact; + public function GetContactByID($mID, bool $bIsStrID = false) : ?Classes\Contact; public function GetSuggestions(string $sSearch, int $iLimit = 20) : array; 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 453e02026..8c7becdcb 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 @@ -28,34 +28,26 @@ trait CardDAV $aResponse = null; try { - $this->oLogger->Write('PROPFIND '.$sPath, \LOG_INFO, 'DAV'); - $aResponse = $oClient->propFind($sPath, array( '{DAV:}getlastmodified', '{DAV:}resourcetype', '{DAV:}getetag' ), 1); - -// $this->oLogger->WriteDump($aResponse); } catch (\Throwable $oException) { - $this->oLogger->WriteException($oException); + $this->logException($oException); } /** * find every *.vcf with empty */ - if (\is_array($aResponse)) - { + if (\is_array($aResponse)) { $mResult = array(); - foreach ($aResponse as $sKey => $aItem) - { + foreach ($aResponse as $sKey => $aItem) { $sKey = \rtrim(\trim($sKey), '\\/'); - if (!empty($sKey) && is_array($aItem)) - { - if (isset($aItem['{DAV:}getetag'])) - { + if (!empty($sKey) && is_array($aItem)) { + if (isset($aItem['{DAV:}getetag'])) { $aMatch = array(); if (\preg_match('/\/([^\/?]+)$/', $sKey, $aMatch) && !empty($aMatch[1]) && !static::hasDAVCollection($aItem)) @@ -72,14 +64,11 @@ trait CardDAV 'changed' => 0 ); - if (isset($aItem['{DAV:}getlastmodified'])) - { + if (isset($aItem['{DAV:}getlastmodified'])) { $mResult[$sKeyID]['lastmodified'] = $aItem['{DAV:}getlastmodified']; $mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString( $aItem['{DAV:}getlastmodified']); - } - else - { + } else { $mResult[$sKeyID]['changed'] = \MailSo\Base\DateTimeHelper::TryToParseSpecEtagFormat($mResult[$sKeyID]['etag']); $mResult[$sKeyID]['lastmodified'] = 0 < $mResult[$sKeyID]['changed'] ? \gmdate('c', $mResult[$sKeyID]['changed']) : ''; @@ -99,35 +88,24 @@ trait CardDAV { \MailSo\Base\Utils::ResetTimeLimit(); - $this->oLogger->Write($sCmd.' '.$sUrl.(('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData ? ' ('.\strlen($mData).')' : ''), + $this->logWrite($sCmd.' '.$sUrl.(('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData ? ' ('.\strlen($mData).')' : ''), \LOG_INFO, 'DAV'); -// if ('PUT' === $sCmd || 'POST' === $sCmd) -// { -// $this->oLogger->Write($mData, \LOG_INFO, 'DAV'); -// } - try { - if (('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData) - { + if (('PUT' === $sCmd || 'POST' === $sCmd) && null !== $mData) { return $oClient->request($sCmd, $sUrl, $mData, array( 'Content-Type' => 'text/vcard; charset=utf-8' )); } - else - { - return $oClient->request($sCmd, $sUrl); - } - -// if ('GET' === $sCmd) -// { + return $oClient->request($sCmd, $sUrl); +// if ('GET' === $sCmd) { // $this->oLogger->WriteDump($aResponse, \LOG_INFO, 'DAV'); // } } catch (\Throwable $oException) { - $this->oLogger->WriteException($oException); + $this->logException($oException); } return null; @@ -135,239 +113,179 @@ trait CardDAV private function detectionPropFind(DAVClient $oClient, string $sPath) : ?array { - $aResponse = null; - try { - $this->oLogger->Write('PROPFIND '.$sPath, \LOG_INFO, 'DAV'); - - $aResponse = $oClient->propFind($sPath, array( + return $oClient->propFind($sPath, array( '{DAV:}current-user-principal', '{DAV:}resourcetype', '{DAV:}displayname', '{urn:ietf:params:xml:ns:carddav}addressbook-home-set' ), 1); - -// $this->oLogger->WriteDump($aResponse); } catch (\Throwable $oException) { - $this->oLogger->WriteException($oException); + $this->logException($oException); } - return $aResponse; + return null; } - private function getContactsPaths(DAVClient $oClient, string $sUser, string $sPassword, string $sProxy = '') : array + private function getContactsPaths(DAVClient $oClient, string $sPath, string $sUser, string $sPassword, string $sProxy = '') : array { $aContactsPaths = array(); $sCurrentUserPrincipal = ''; $sAddressbookHomeSet = ''; -// [{DAV:}current-user-principal] => /cloud/remote.php/carddav/principals/admin/ -// [{urn:ietf:params:xml:ns:carddav}addressbook-home-set] => /cloud/remote.php/carddav/addressbooks/admin/ - - $aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav'); + $aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav') + ?: $this->detectionPropFind($oClient, $sPath); $sNextPath = ''; $sFirstNextPath = ''; - if (\is_array($aResponse)) - { - foreach ($aResponse as $sKey => $aItem) - { - if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) && - false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy')) + if (\is_array($aResponse)) { + foreach ($aResponse as $sPropPath => $aItem) { + if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) + && false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy')) { - $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']; + $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']; continue; } - if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal'])) - { - $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']; + if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']['{DAV:}href'])) { + $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']['{DAV:}href']; continue; } - if (!empty($sKey)) - { - if (empty($sFirstNextPath)) - { - $sFirstNextPath = $sKey; + if (!empty($sPropPath)) { + if (empty($sFirstNextPath)) { + $sFirstNextPath = $sPropPath; } - if (empty($sNextPath)) - { - if (static::hasDAVCollection($aItem)) - { - $sNextPath = $sKey; + if (empty($sNextPath)) { + if (static::hasDAVCollection($aItem)) { + $sNextPath = $sPropPath; continue; } } } } - if (empty($sNextPath) && empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet) && !empty($sFirstNextPath)) - { + if (empty($sNextPath) && empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet) && !empty($sFirstNextPath)) { $sNextPath = $sFirstNextPath; } } - if (empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet)) - { - if (empty($sNextPath)) - { + if (empty($sCurrentUserPrincipal) && empty($sAddressbookHomeSet)) { + if (empty($sNextPath)) { return $aContactsPaths; } - else - { - if (\preg_match('/^http[s]?:\/\//i', $sNextPath)) - { - $oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy); - if ($oClient) - { - $sNextPath = $oClient->__UrlPath__; - } - else - { - return $aContactsPaths; - } - } - - $aResponse = $this->detectionPropFind($oClient, $sNextPath); - if (\is_array($aResponse)) - { - foreach ($aResponse as $sKey => $aItem) - { - if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) && - false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy')) - { - $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']; - continue; - } - - if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal'])) - { - $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']; - continue; - } - } - } - } - } - - if (empty($sAddressbookHomeSet)) - { - if (empty($sCurrentUserPrincipal)) - { - return $aContactsPaths; - } - else - { - if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal)) - { - $oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy); - if ($oClient) - { - $sCurrentUserPrincipal = $oClient->__UrlPath__; - } - else - { - return $aContactsPaths; - } - } - - $aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal); - if (\is_array($aResponse)) - { - foreach ($aResponse as $sKey => $aItem) - { - if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) && - false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy')) - { - $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']; - continue; - } - } - } - } - } - - if (empty($sAddressbookHomeSet)) - { - return $aContactsPaths; - } - else - { - if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet)) - { - $oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy); - if ($oClient) - { - $sAddressbookHomeSet = $oClient->__UrlPath__; - } - else - { + if (\preg_match('/^http[s]?:\/\//i', $sNextPath)) { + $oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy); + if (!$oClient) { return $aContactsPaths; } + $sNextPath = $oClient->__UrlPath__; } - $aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet); - if (\is_array($aResponse)) - { - foreach ($aResponse as $sKey => $aItem) - { - if (!empty($sKey) && static::hasDAVCollection($aItem) - && \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype'])) - { - $aContactsPaths[$sKey] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : ''; + if ($sPath != $sNextPath) { + $aResponse = $this->detectionPropFind($oClient, $sNextPath); + if (\is_array($aResponse)) { + foreach ($aResponse as $aItem) { + if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) && + false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy')) + { + $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']; + continue; + } + + if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']['{DAV:}href'])) { + $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']['{DAV:}href']; + continue; + } } } } } + if (empty($sAddressbookHomeSet)) { + if (empty($sCurrentUserPrincipal)) { + return $aContactsPaths; + } + if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal)) { + $oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy); + if (!$oClient) { + return $aContactsPaths; + } + $sCurrentUserPrincipal = $oClient->__UrlPath__; + } + + $aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal); + if (\is_array($aResponse)) { + foreach ($aResponse as $aItem) { + if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) && + false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy')) + { + $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']; + continue; + } + } + } + } + + if (empty($sAddressbookHomeSet)) { + return $aContactsPaths; + } + if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet)) { + $oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy); + if (!$oClient) { + return $aContactsPaths; + } + $sAddressbookHomeSet = $oClient->__UrlPath__; + } + + $aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet); + if (\is_array($aResponse)) { + foreach ($aResponse as $sPropPath => $aItem) { + if (!empty($sPropPath) && static::hasDAVCollection($aItem) + && \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype'])) + { + $aContactsPaths[$sPropPath] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : ''; + } + } + } + return $aContactsPaths; } + /** + * Checks if remote path resourcetype is an addressbook + */ private function checkContactsPath(DAVClient $oClient, string $sPath) : bool { - if (!$oClient) - { - return false; - } - - $this->oLogger->Write('PROPFIND '.$sPath, \LOG_INFO, 'DAV'); - $aResponse = null; try { $aResponse = $oClient->propFind($sPath, array( '{DAV:}resourcetype' ), 1); - -// $this->oLogger->WriteDump($aResponse); } catch (\Throwable $oException) { - $this->oLogger->WriteException($oException); + $this->logException($oException); } $bGood = false; - if (\is_array($aResponse)) - { - foreach ($aResponse as $sKey => $aItem) - { + if (\is_array($aResponse)) { + foreach ($aResponse as $sKey => $aItem) { if (!empty($sKey) && static::hasDAVCollection($aItem) && \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype'])) { $bGood = true; } } - } - - if ($bGood) - { - $oClient->__UrlPath__ = $sPath; + if ($bGood) { + $oClient->__UrlPath__ = $sPath; + } } return $bGood; @@ -375,8 +293,7 @@ trait CardDAV private function getDavClientFromUrl(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : DAVClient { - if (!\preg_match('/^http[s]?:\/\//i', $sUrl)) - { + if (!\preg_match('/^http[s]?:\/\//i', $sUrl)) { $sUrl = \preg_replace('/^fruux\.com/i', 'dav.fruux.com', $sUrl); $sUrl = \preg_replace('/^icloud\.com/i', 'contacts.icloud.com', $sUrl); $sUrl = \preg_replace('/^gmail\.com/i', 'google.com', $sUrl); @@ -384,8 +301,7 @@ trait CardDAV } $aUrl = \parse_url($sUrl); - if (!\is_array($aUrl)) - { + if (!\is_array($aUrl)) { $aUrl = array(); } @@ -400,10 +316,9 @@ trait CardDAV 'password' => $sPassword ); - $this->oLogger->AddSecret($sPassword); + $this->logMask($sPassword); - if (!empty($sProxy)) - { + if (!empty($sProxy)) { $aSettings['proxy'] = $sProxy; } @@ -412,7 +327,7 @@ trait CardDAV $oClient->__UrlPath__ = $aUrl['path']; - $this->oLogger->Write('DavClient: User: '.$aSettings['userName'].', Url: '.$sUrl, \LOG_INFO, 'DAV'); + $this->logWrite('DavClient: User: '.$aSettings['userName'].', Url: '.$sUrl, \LOG_INFO, 'DAV'); return $oClient; } @@ -430,8 +345,7 @@ trait CardDAV $aMatch = array(); $sUserAddressBookNameName = ''; - if (\preg_match('/\|(.+)$/', $sUrl, $aMatch) && !empty($aMatch[1])) - { + if (\preg_match('/\|(.+)$/', $sUrl, $aMatch) && !empty($aMatch[1])) { $sUserAddressBookNameName = \trim($aMatch[1]); $sUserAddressBookNameName = \mb_strtolower($sUserAddressBookNameName); @@ -443,37 +357,30 @@ trait CardDAV $sPath = $oClient->__UrlPath__; $bGood = true; - if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath)) - { - $sNewPath = ''; - - $aPaths = $this->getContactsPaths($oClient, $sUser, $sPassword, $sProxy); + if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath)) { + /** + * Path is not an addressbook, try to find it + */ + $aPaths = $this->getContactsPaths($oClient, $sPath, $sUser, $sPassword, $sProxy); $this->oLogger->WriteDump($aPaths); - if (\is_array($aPaths)) - { - if (1 < \count($aPaths)) - { - if ('' !== $sUserAddressBookNameName) - { - foreach ($aPaths as $sKey => $sValue) - { + $sNewPath = ''; + if (\is_array($aPaths)) { + if (1 < \count($aPaths)) { + if ('' !== $sUserAddressBookNameName) { + foreach ($aPaths as $sKey => $sValue) { $sValue = \mb_strtolower(\trim($sValue)); - if ($sValue === $sUserAddressBookNameName) - { + if ($sValue === $sUserAddressBookNameName) { $sNewPath = $sKey; break; } } } - if (empty($sNewPath)) - { - foreach ($aPaths as $sKey => $sValue) - { + if (empty($sNewPath)) { + foreach ($aPaths as $sKey => $sValue) { $sValue = \mb_strtolower($sValue); - if (\in_array($sValue, array('contacts', 'default', 'addressbook', 'address book'))) - { + if (\in_array($sValue, array('contacts', 'default', 'addressbook', 'address book'))) { $sNewPath = $sKey; break; } @@ -481,19 +388,18 @@ trait CardDAV } } - if (empty($sNewPath)) - { - foreach ($aPaths as $sKey => $sValue) - { + if (empty($sNewPath)) { + foreach ($aPaths as $sKey => $sValue) { $sNewPath = $sKey; break; } } } - $sPath = $sNewPath; - - $bGood = $this->checkContactsPath($oClient, $sPath); + $bGood = $sNewPath && $this->checkContactsPath($oClient, $sNewPath); + if (!$bGood) { + $this->logWrite('Contacts path not found at: '.$sPath, \LOG_INFO, 'DAV'); + } } return $bGood ? $oClient : null; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php index 316764420..3d88ba81d 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Classes/Contact.php @@ -2,43 +2,26 @@ namespace RainLoop\Providers\AddressBook\Classes; +use Sabre\VObject\Component\VCard; + class Contact implements \JsonSerializable { - /** - * @var string - */ - public $id = ''; + public string $id = ''; + + public string $IdContactStr = ''; + +// public string $Display; + + public int $Changed; + + public bool $ReadOnly = false; /** - * @var string - */ - public $IdContactStr = ''; - - /** - * @var string - */ -// public $Display; - - /** - * @var int - */ - public $Changed; - - /** - * @var bool - */ - public $ReadOnly = false; - - /** - * @var string * Used for CardDAV synchronization */ - public $Etag = ''; + public string $Etag = ''; - /** - * @var \Sabre\VObject\Component\VCard - */ - protected $vCard = null; + protected ?VCard $vCard = null; function __construct() { @@ -62,8 +45,15 @@ class Contact implements \JsonSerializable return \array_unique($aResult); } */ - public function setVCard(\Sabre\VObject\Component\VCard $oVCard) : void + public function setVCard(VCard $oVCard) : void { + if ($oVCard->PHOTO && $oVCard->PHOTO->parameters['ENCODING']) { + $oVCard->VERSION = '3.0'; + } + if (VCard::VCARD40 != $oVCard->getDocumentType()) { + $oVCard = $oVCard->convert(VCard::VCARD40); + } + // KDE KAddressBook entry and used by SnappyMail // https://github.com/sabre-io/vobject/issues/589 $oVCard->select('X-CRYPTO') diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php index 117570c01..6a125ebfd 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Legacy.php @@ -20,11 +20,9 @@ class Legacy if (\strlen($sValue)) { $oTypes = $oProp['TYPE']; $aTypes = $oTypes ? $oTypes->getParts() : array(); - $pref = 100; - if (0 < $oProp['PREF']) { - $pref = (int) $oProp['PREF']; - } - $aTmp[\substr(1000+$pref,-3) . $sValue] = new Property($iType, $sValue, \implode(',', $aTypes)); + $pref = empty($oProp['PREF']) ? 100 : \min(100, \max(1, $oProp['PREF']->getValue())); + $pref = \str_pad($pref, 3, '0', \STR_PAD_LEFT); + $aTmp[$pref . $sValue] = new Property($iType, $sValue, \implode(',', $aTypes)); } } \ksort($aTmp); 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 9a37ee8b2..fbf5aeebc 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php @@ -9,32 +9,14 @@ use ; class PdoAddressBook - extends \RainLoop\Common\PdoAbstract + extends \RainLoop\Pdo\Base implements AddressBookInterface { use CardDAV; private $iUserID = 0; - /** - * @var string - */ - private $sDsn; - - /** - * @var string - */ - private $sDsnType; - - /** - * @var string - */ - private $sUser; - - /** - * @var string - */ - private $sPassword; + private \RainLoop\Pdo\Settings $settings; private static $aSearchInFields = [ PropertyType::EMAIl, @@ -46,29 +28,38 @@ class PdoAddressBook public function __construct() { $oConfig = \RainLoop\Api::Config(); - $sDsnType = static::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); - if ('sqlite' === $sDsnType) { - $sUser = $sPassword = ''; + $oSettings = new \RainLoop\Pdo\Settings; + $oSettings->driver = static::validPdoType($oConfig->Get('contacts', 'type', 'sqlite')); + if ('sqlite' === $oSettings->driver) { $sDsn = 'sqlite:' . APP_PRIVATE_DATA . 'AddressBook.sqlite'; /* // TODO: use local db? - $homedir = \RainLoop\Api::Actions()->StorageProvider()->GenerateFilePath( - $oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::ROOT - ); - $sDsn = 'sqlite:' . $homedir . '/AddressBook.sqlite'; + $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'); + } + $sDsn = 'sqlite:' . $homedir . 'AddressBook.sqlite'; + } */ } else { $sDsn = \trim($oConfig->Get('contacts', 'pdo_dsn', '')); - $sUser = \trim($oConfig->Get('contacts', 'pdo_user', '')); - $sPassword = (string)$oConfig->Get('contacts', 'pdo_password', ''); - $sDsn = $sDsnType . ':' . \preg_replace('/^[a-z]+:/', '', $sDsn); + $oSettings->user = \trim($oConfig->Get('contacts', 'pdo_user', '')); + $oSettings->password = (string)$oConfig->Get('contacts', 'pdo_password', ''); + $sDsn = $oSettings->driver . ':' . \preg_replace('/^[a-z]+:/', '', $sDsn); + if ('mysql' === $oSettings->driver) { + $oSettings->sslCa = \trim($oConfig->Get('contacts', 'mysql_ssl_ca', '')); + $oSettings->sslVerify = !!$oConfig->Get('contacts', 'mysql_ssl_verify', true); + $oSettings->sslCiphers = \trim($oConfig->Get('contacts', 'mysql_ssl_ciphers', '')); + } } - $this->sDsn = $sDsn; - $this->sUser = $sUser; - $this->sPassword = $sPassword; - $this->sDsnType = $sDsnType; + $oSettings->dsn = $sDsn; + $this->settings = $oSettings; $this->bExplain = false; // debug } @@ -82,7 +73,7 @@ class PdoAddressBook public function IsSupported() : bool { $aDrivers = static::getAvailableDrivers(); - return \is_array($aDrivers) && \in_array($this->sDsnType, $aDrivers); + return \is_array($aDrivers) && \in_array($this->settings->driver, $aDrivers); } public function SetEmail(string $sEmail) : bool @@ -101,9 +92,12 @@ class PdoAddressBook private function prepareDatabaseSyncData() : array { $aResult = array(); - $oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag FROM rainloop_ab_contacts WHERE id_user = :id_user', array( - ':id_user' => array($this->iUserID, \PDO::PARAM_INT) - )); + $oStmt = $this->prepareAndExecute('SELECT id_contact, id_contact_str, changed, deleted, etag + FROM rainloop_ab_contacts + WHERE id_user = :id_user + ORDER BY deleted DESC', + array(':id_user' => array($this->iUserID, \PDO::PARAM_INT)) + ); if ($oStmt) { $aFetch = $oStmt->fetchAll(\PDO::FETCH_ASSOC); @@ -150,34 +144,41 @@ class PdoAddressBook return false; } $time = \microtime(true) - $time; - \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Fetched remote data in {$time} seconds"]); + \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Received ".\count($aRemoteSyncData)." remote contacts in {$time} seconds"]); + \SnappyMail\Log::info('PdoAddressBook', \count($aRemoteSyncData) . ' remote contacts'); - $aDatabaseSyncData = $this->prepareDatabaseSyncData(); + $aLocalSyncData = $this->prepareDatabaseSyncData(); + \SnappyMail\Log::info('PdoAddressBook', \count($aLocalSyncData) . ' local contacts'); // $this->oLogger->WriteDump($aRemoteSyncData); -// $this->oLogger->WriteDump($aDatabaseSyncData); +// $this->oLogger->WriteDump($aLocalSyncData); $bReadWrite = $this->isDAVReadWrite(); // Delete remote when Mode = read + write if ($bReadWrite) { \SnappyMail\Log::info('PdoAddressBook', 'Sync() is import and export'); - foreach ($aDatabaseSyncData as $sKey => $aData) { + $iCount = 0; + foreach ($aLocalSyncData as $sKey => $aData) { if ($aData['deleted']) { - unset($aDatabaseSyncData[$sKey]); + ++$iCount; + unset($aLocalSyncData[$sKey]); if (isset($aRemoteSyncData[$sKey], $aRemoteSyncData[$sKey]['vcf'])) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Delete remote {$sKey}"]); $this->davClientRequest($oClient, 'DELETE', $sPath.$aRemoteSyncData[$sKey]['vcf']); } } } + if ($iCount) { + \SnappyMail\Log::info('PdoAddressBook', $iCount . ' remote contacts removed'); + } } else { \SnappyMail\Log::info('PdoAddressBook', 'Sync() is import only'); } - // Delete from db + // Delete local $aIdsForDeletion = array(); - foreach ($aDatabaseSyncData as $sKey => $aData) { + foreach ($aLocalSyncData as $sKey => $aData) { if (!empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) { $aIdsForDeletion[] = $aData['id_contact']; } @@ -185,39 +186,31 @@ class PdoAddressBook if (\count($aIdsForDeletion)) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>'Delete local ' . \implode(', ', $aIdsForDeletion)]); $this->DeleteContacts($aIdsForDeletion); + \SnappyMail\Log::info('PdoAddressBook', \count($aIdsForDeletion) . ' local contacts removed'); unset($aIdsForDeletion); } $this->flushDeletedContacts(); - //+++new or newer (from db) - foreach ($aDatabaseSyncData as $sKey => $aData) { - if ((empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) // new - || - (!empty($aData['etag']) && isset($aRemoteSyncData[$sKey]) && // newer - $aRemoteSyncData[$sKey]['etag'] !== $aData['etag'] && - $aRemoteSyncData[$sKey]['changed'] < $aData['changed'] - ) - ) { - \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update remote {$sKey}"]); - $mID = $aData['id_contact']; - $oContact = $this->GetContactByID($mID); - if ($oContact) { - $sExsistensBody = ''; - $mExsistenRemoteID = isset($aRemoteSyncData[$sKey]['vcf']) && !empty($aData['etag']) ? $aRemoteSyncData[$sKey]['vcf'] : ''; - if (\strlen($mExsistenRemoteID)) { - $oResponse = $this->davClientRequest($oClient, 'GET', $sPath.$mExsistenRemoteID); - if ($oResponse) { - $sExsistensBody = \trim($oResponse->body); - } - -// $this->oLogger->WriteDump($sExsistensBody); - } - - // Add remote when Mode = read + write - if ($sExsistensBody && $bReadWrite) { + // local is new or newer + if ($bReadWrite) { + foreach ($aLocalSyncData as $sKey => $aData) { + if ((empty($aData['etag']) && !isset($aRemoteSyncData[$sKey])) // new + // newer + || (!empty($aData['etag']) && isset($aRemoteSyncData[$sKey]) && + $aRemoteSyncData[$sKey]['etag'] !== $aData['etag'] && + $aRemoteSyncData[$sKey]['changed'] < $aData['changed'] + ) + ) { + \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update remote {$sKey}"]); + $mID = $aData['id_contact']; + $oContact = $this->GetContactByID($mID); + if ($oContact) { + $sRemoteID = isset($aRemoteSyncData[$sKey]['vcf']) && !empty($aData['etag']) + ? $aRemoteSyncData[$sKey]['vcf'] : ''; + \SnappyMail\Log::info('PdoAddressBook', "Update contact {$sKey} in DAV"); $oResponse = $this->davClientRequest($oClient, 'PUT', - $sPath.(\strlen($mExsistenRemoteID) ? $mExsistenRemoteID : $oContact->IdContactStr.'.vcf'), + $sPath . ($sRemoteID ?: $oContact->IdContactStr.'.vcf'), $oContact->vCard->serialize() . "\r\n\r\n"); if ($oResponse) { $sEtag = \trim(\trim($oResponse->getHeader('etag')), '"\''); @@ -233,25 +226,26 @@ class PdoAddressBook ) ); } + } else { + \SnappyMail\Log::warning('PdoAddressBook', "Update/create remote failed"); } + } else { + \SnappyMail\Log::warning('PdoAddressBook', "Local contact {$sKey} not found"); } + unset($oContact); } - - unset($oContact); } } - //---new - //+++new or newer (from carddav) + // remote is new or newer foreach ($aRemoteSyncData as $sKey => $aData) { - if (!isset($aDatabaseSyncData[$sKey]) // new - || - ($aDatabaseSyncData[$sKey]['etag'] !== $aData['etag'] && // newer - $aDatabaseSyncData[$sKey]['changed'] < $aData['changed']) + if (!isset($aLocalSyncData[$sKey]) // new + // newer + || ($aLocalSyncData[$sKey]['etag'] !== $aData['etag'] && $aLocalSyncData[$sKey]['changed'] < $aData['changed']) ) { \SnappyMail\HTTP\Stream::JSON(['messsage'=>"Update local {$sKey}"]); - $mExistingContactID = isset($aDatabaseSyncData[$sKey]['id_contact']) ? - $aDatabaseSyncData[$sKey]['id_contact'] : ''; + + $oVCard = null; $oResponse = $this->davClientRequest($oClient, 'GET', $sPath.$aData['vcf']); if ($oResponse) { @@ -263,42 +257,40 @@ class PdoAddressBook } if (!empty($sBody)) { - $oVCard = null; try { $oVCard = \Sabre\VObject\Reader::read($sBody); - } - catch (\Throwable $oExc) { - if ($this->oLogger) { - $this->oLogger->WriteException($oExc); - $this->oLogger->WriteDump($sBody); - } - } - - if ($oVCard instanceof VCard) { - $oVCard->UID = $aData['uid']; - - $oContact = null; - if ($mExistingContactID) { - $oContact = $this->GetContactByID($mExistingContactID); - } - if (!$oContact) { - $oContact = new Contact(); - } - - $oContact->setVCard($oVCard); - - $sEtag = \trim($oResponse->getHeader('etag'), " \n\r\t\v\x00\"'"); - if (!empty($sEtag)) { - $oContact->Etag = $sEtag; - } - - $this->ContactSave($oContact); - unset($oContact); -// } else if ($this->oLogger) { -// $this->oLogger->WriteDump($sBody); + } catch (\Throwable $oExc) { + $this->logException($oExc); + $this->oLogger && $this->oLogger->WriteDump($sBody); } } } + + if ($oVCard instanceof VCard) { + $oVCard->UID = $aData['uid']; + + $oContact = empty($aLocalSyncData[$sKey]['id_contact']) + ? null + : $this->GetContactByID($aLocalSyncData[$sKey]['id_contact']); + if ($oContact) { + \SnappyMail\Log::info('PdoAddressBook', "Update local contact {$sKey}"); + } else { + \SnappyMail\Log::info('PdoAddressBook', "Create local contact {$sKey}"); + $oContact = new Contact(); + } + + $oContact->setVCard($oVCard); + + $sEtag = \trim($oResponse->getHeader('etag'), " \n\r\t\v\x00\"'"); + if (!empty($sEtag)) { + $oContact->Etag = $sEtag; + } + + $this->ContactSave($oContact); + unset($oContact); + } else { + \SnappyMail\Log::error('PdoAddressBook', "Import remote contact {$sKey} failed"); + } } } @@ -308,6 +300,7 @@ class PdoAddressBook public function Export(string $sType = 'vcf') : bool { if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'Export() invalid $iUserID'); return false; } @@ -317,16 +310,21 @@ class PdoAddressBook $aDatabaseSyncData = $this->prepareDatabaseSyncData(); if (\count($aDatabaseSyncData)) { foreach ($aDatabaseSyncData as $mData) { - if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { - $oContact = $this->GetContactByID($mData['id_contact']); - if ($oContact) { - if ($rCsv) { - Utils::VCardToCsv($rCsv, $oContact, $bCsvHeader); - $bCsvHeader = false; - } else { - echo $oContact->vCard->serialize(); + try { +// if ($mData && isset($mData['id_contact'], $mData['deleted']) && !$mData['deleted']) { + if ($mData && !empty($mData['id_contact'])) { + $oContact = $this->GetContactByID($mData['id_contact']); + if ($oContact) { + if ($rCsv) { + Utils::VCardToCsv($rCsv, $oContact->vCard, $bCsvHeader); + $bCsvHeader = false; + } else { + echo $oContact->vCard->serialize(); + } } } + } catch (\Throwable $oExc) { + $this->logException($oExc); } } } @@ -337,6 +335,7 @@ class PdoAddressBook public function ContactSave(Contact $oContact) : bool { if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'ContactSave() invalid $iUserID'); return false; } @@ -436,6 +435,7 @@ class PdoAddressBook public function DeleteContacts(array $aContactIds) : bool { if (1 > $this->iUserID) { + \SnappyMail\Log::warning('PdoAddressBook', 'DeleteContacts() invalid $iUserID'); return false; } @@ -531,12 +531,16 @@ class PdoAddressBook $sUid = $sFirstName = $sLastName = $sMiddleName = $sSuffix = $sPrefix = ''; $iPrevId = $iId; } - if (!isset($aVCards[$iId])) { - $aVCards[$iId] = new VCard; + if (isset($aVCards[$iId])) { + $oVCard = $aVCards[$iId]; + } else { + $oVCard = new VCard; +// $oVCard = $oVCard->convert(VCard::VCARD40); + $oVCard->VERSION = '4.0'; + $oVCard->PRODID = 'SnappyMail-'.APP_VERSION; + $aVCards[$iId] = $oVCard; } $oVCard = $aVCards[$iId]; - $oVCard->VERSION = '4.0'; - $oVCard->PRODID = 'SnappyMail-'.APP_VERSION; $sPropValue = (string) $aItem['prop_value']; $aTypes = array(); if (!empty($aItem['prop_type_str'])) { @@ -690,6 +694,40 @@ class PdoAddressBook return []; } + /** + * @param mixed $mID + */ + public function GetContactByEmail(string $sEmail) : ?Contact + { + $sLowerSearch = $this->specialConvertSearchValueLower($sEmail); + + $sSql = 'SELECT + DISTINCT id_contact + FROM rainloop_ab_properties + WHERE id_user = :id_user + AND prop_type = '.PropertyType::JCARD.' + AND ('. + 'prop_value LIKE :search ESCAPE \'=\'' + . (\strlen($sLowerSearch) ? ' OR (prop_value_lower <> \'\' AND prop_value_lower LIKE :search_lower ESCAPE \'=\')' : ''). + ')'; + $aParams = array( + ':id_user' => array($this->iUserID, \PDO::PARAM_INT), + ':search' => array($this->specialConvertSearchValue($sEmail, '='), \PDO::PARAM_STR) + ); + if (\strlen($sLowerSearch)) { + $aParams[':search_lower'] = array($sLowerSearch, \PDO::PARAM_STR); + } + + $oContact = null; + $iIdContact = 0; + + $aContacts = $this->getContactsFromPDO( + $this->prepareAndExecute($sSql, $aParams) + ); + + return $aContacts ? $aContacts[0] : null; + } + /** * @param mixed $mID */ @@ -706,7 +744,7 @@ class PdoAddressBook p.prop_value as jcard FROM rainloop_ab_contacts AS c LEFT JOIN rainloop_ab_properties AS p ON (p.id_contact = c.id_contact AND p.prop_type = :prop_type) - WHERE c.deleted = 0 AND c.id_user = :id_user'; + WHERE c.id_user = :id_user AND c.deleted = 0'; $aParams = array( ':id_user' => array($this->iUserID, \PDO::PARAM_INT), @@ -1023,7 +1061,7 @@ class PdoAddressBook $sResult = ''; try { $this->SyncDatabase(); - if (0 >= $this->getVersion($this->sDsnType.'-ab-version')) { + if (0 >= $this->getVersion($this->settings->driver.'-ab-version')) { $sResult = 'Unknown database error'; } } @@ -1170,10 +1208,10 @@ SQLITEINITIAL; } $mCache = false; - switch ($this->sDsnType) { + switch ($this->settings->driver) { case 'mysql': - $mCache = $this->dataBaseUpgrade($this->sDsnType.'-ab-version', array( - 1 => $this->getInitialTablesArray($this->sDsnType), + $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;' ), @@ -1190,8 +1228,8 @@ SQLITEINITIAL; )); break; case 'pgsql': - $mCache = $this->dataBaseUpgrade($this->sDsnType.'-ab-version', array( - 1 => $this->getInitialTablesArray($this->sDsnType), + $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 \'\';' ), @@ -1200,8 +1238,8 @@ SQLITEINITIAL; )); break; case 'sqlite': - $mCache = $this->dataBaseUpgrade($this->sDsnType.'-ab-version', array( - 1 => $this->getInitialTablesArray($this->sDsnType), + $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 \'\';' ), @@ -1253,9 +1291,15 @@ SQLITEINITIAL; (string) \mb_strtolower($sSearch)).'%'; } - protected function getPdoAccessData() : array + protected function getPdoSettings() : \RainLoop\Pdo\Settings { - return array($this->sDsnType, $this->sDsn, $this->sUser, $this->sPassword); + $sSslCa = $this->settings->sslCa; + if ($sSslCa && !\is_file($sSslCa)) { + $sFile = \APP_PRIVATE_DATA . 'configs/contacts_mysql_ssl_ca.pem'; +// $sSslCa = (\is_file($sFile) || \file_put_contents($sFile, $sSslCa)) ? $sFile : ''; + $this->settings->sslCa = \file_put_contents($sFile, $sSslCa) ? $sFile : ''; + } + return $this->settings; } protected function getUserId(string $sEmail, bool $bSkipInsert = false, bool $bCache = true) : int diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php index 00834404d..983e67aa7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/Utils.php @@ -2,6 +2,8 @@ namespace RainLoop\Providers\AddressBook; +use Sabre\VObject\Component\VCard; + class Utils { private static $aMap = array( @@ -22,20 +24,29 @@ class Utils 'namesuffix' => 4, 'shortname' => 'NICKNAME', 'nickname' => 'NICKNAME', + 'birthday' => 'BDAY', + + 'mobile' => array('TEL', 'CELL'), + 'mobilephone' => array('TEL', 'CELL'), + + 'businessemail' => array('EMAIL', 'WORK'), + 'businessemail2' => array('EMAIL', 'WORK'), + 'businessemail3' => array('EMAIL', 'WORK'), 'businessphone' => array('TEL', 'WORK'), 'businessphone2' => array('TEL', 'WORK'), 'businessphone3' => array('TEL', 'WORK'), + 'businessmobile' => array('TEL', 'WORK,CELL'), + 'businessmobilephone' => array('TEL', 'WORK,CELL'), + 'businessweb' => array('URL', 'WORK'), + 'businesswebpage' => array('URL', 'WORK'), + 'businesswebsite' => array('URL', 'WORK'), 'companyphone' => array('TEL', 'WORK'), 'companymainphone' => array('TEL', 'WORK'), + + 'primaryphone' => array('TEL', 'PREF,HOME'), 'homephone' => array('TEL', 'HOME'), 'homephone2' => array('TEL', 'HOME'), 'homephone3' => array('TEL', 'HOME'), - 'mobile' => array('TEL', 'CELL'), - 'mobilephone' => array('TEL', 'CELL'), - 'businessmobile' => array('TEL', 'WORK,CELL'), - 'businessmobilephone' => array('TEL', 'WORK,CELL'), - 'otherphone' => 'TEL', - 'primaryphone' => array('TEL', 'PREF,HOME'), 'email' => array('EMAIL', 'HOME'), 'email2' => array('EMAIL', 'HOME'), 'email3' => array('EMAIL', 'HOME'), @@ -48,53 +59,73 @@ class Utils 'emailaddress' => array('EMAIL', 'HOME'), 'email2address' => array('EMAIL', 'HOME'), 'email3address' => array('EMAIL', 'HOME'), - 'otheremail' => 'EMAIL', - 'businessemail' => array('EMAIL', 'WORK'), - 'businessemail2' => array('EMAIL', 'WORK'), - 'businessemail3' => array('EMAIL', 'WORK'), 'personalemail' => array('EMAIL', 'HOME'), 'personalemail2' => array('EMAIL', 'HOME'), 'personalemail3' => array('EMAIL', 'HOME'), + 'personalwebsite' => array('URL', 'HOME'), + + 'otheremail' => 'EMAIL', + 'otherphone' => 'TEL', 'notes' => 'NOTE', 'web' => 'URL', - 'businessweb' => array('URL', 'WORK'), 'webpage' => 'URL', - 'businesswebpage' => array('URL', 'WORK'), - 'website' => 'URL', - 'businesswebsite' => array('URL', 'WORK'), - 'personalwebsite' => 'URL', - 'birthday' => 'BDAY' + 'website' => 'URL' +/* + TODO: + 'company' => '', + 'department' => '', + 'jobtitle' => '', + 'officelocation' => '', + 'homestreet' => '', + 'homecity' => '', + 'homestate' => '', + 'homepostalcode' => '', + 'homecountry' => '', + 'businessstreet' => '', + 'businesscity' => '', + 'businessstate' => '', + 'businesspostalcode' => '', + 'businesscountry' => '', +*/ ); - public static function CsvArrayToContacts(array $aCsvData) : iterable + public static function CsvStreamToContacts(/*resource*/ $rFile, string $sDelimiter) : iterable { - foreach ($aCsvData as $aItem) { + \setlocale(LC_CTYPE, 'en_US.UTF-8'); + + $aHeaders = \fgetcsv($rFile, 5000, $sDelimiter, '"'); + if (!$aHeaders || 3 >= \count($aHeaders)) { + return; + } + foreach ($aHeaders as $iIndex => $sItemName) { + $sItemName = \MailSo\Base\Utils::Utf8Clear($sItemName); + $sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName))); + if (!\array_key_exists($sItemName, VCard::$propertyMap)) { + $sItemName = \strtolower($sItemName); + $sItemName = isset(static::$aMap[$sItemName]) ? static::$aMap[$sItemName] : null; + } + $aHeaders[$iIndex] = $sItemName; + } + + while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) { \MailSo\Base\Utils::ResetTimeLimit(); $iCount = 0; - $oVCard = new \Sabre\VObject\Component\VCard; + $oVCard = new VCard; $aName = ['','','','','']; - foreach ($aItem as $sItemName => $sItemValue) { - $sItemName = \strtoupper(\trim(\preg_replace('/[\s\-]+/', '', $sItemName))); + foreach ($mRow as $iIndex => $sItemValue) { + $sItemName = $aHeaders[$iIndex]; $sItemValue = \trim($sItemValue); - if (!empty($sItemName) && !empty($sItemValue)) { - if (\array_key_exists($sItemName, \Sabre\VObject\Component\VCard::$propertyMap)) { - $mData = $sItemName; + if (isset($sItemName) && !empty($sItemValue)) { + $mType = \is_array($sItemName) ? $sItemName[0] : $sItemName; + ++$iCount; + if (\is_int($mType)) { + $aName[$mType] = $sItemValue; + } else if (\is_array($sItemName)) { + $oVCard->add($mType, $sItemValue, ['type' => $sItemName[1]]); + } else if ('FN' === $mType || 'NICKNAME' === $mType) { + $oVCard->$mType = $sItemValue; } else { - $sItemName = \strtolower($sItemName); - $mData = !empty($sItemName) && isset($aMap[$sItemName]) ? $aMap[$sItemName] : null; - } - if ($mData) { - $mType = \is_array($mData) ? $mData[0] : $mData; - ++$iCount; - if (\is_int($mType)) { - $aName[$mType] = $sItemValue; - } else if (\is_array($mData)) { - $oVCard->add($mType, $sItemValue, ['type' => $mData[1]]); - } else if ('FN' === $mType || 'NICKNAME' === $mType) { - $oVCard->$mType = $sItemValue; - } else { - $oVCard->add($mType, $sItemValue); - } + $oVCard->add($mType, $sItemValue); } } } @@ -109,7 +140,7 @@ class Utils } } - public static function VCardToCsv($stream, Classes\Contact $oContact, bool $bWithHeader = false)/* : int|false*/ + public static function VCardToCsv($stream, VCard $oVCard, bool $bWithHeader = false)/* : int|false*/ { $aData = array(); if ($bWithHeader) { @@ -124,8 +155,6 @@ class Utils )); } - $oVCard = $oContact->vCard; - $aName = isset($oVCard->N) ? $oVCard->N->getParts() : ['','','','','']; $adrHome = $oVCard->getByType('ADR', 'HOME'); @@ -169,16 +198,12 @@ class Utils )); } - public static function VcfFileToContacts(string $sVcfData) : iterable + public static function VcfStreamToContacts(/*resource*/ $rFile) : iterable { - $sVcfData = \trim($sVcfData); - if ("\xef\xbb\xbf" === \substr($sVcfData, 0, 3)) { - $sVcfData = \substr($sVcfData, 3); - } - $oVCardSplitter = new \Sabre\VObject\Splitter\VCard($sVcfData); + $oVCardSplitter = new \Sabre\VObject\Splitter\VCard($rFile); if ($oVCardSplitter) { while ($oVCard = $oVCardSplitter->getNext()) { - if ($oVCard instanceof \Sabre\VObject\Component\VCard) { + if ($oVCard instanceof VCard) { \MailSo\Base\Utils::ResetTimeLimit(); $oContact = new Classes\Contact(); $oContact->setVCard($oVCard); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php index 5fcbf11ca..751dcadaf 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain.php @@ -57,31 +57,19 @@ class Domain extends AbstractProvider public function LoadOrCreateNewFromAction(\RainLoop\Actions $oActions, string $sNameForTest = null) : ?\RainLoop\Model\Domain { - $sName = (string) $oActions->GetActionParam('Name', ''); + $sName = \mb_strtolower((string) $oActions->GetActionParam('name', '')); if (\strlen($sName) && $sNameForTest && !\str_contains($sName, '*')) { $sNameForTest = null; } if (\strlen($sName) || $sNameForTest) { - if (!$sNameForTest && !empty($oActions->GetActionParam('Create', 0)) && $this->Load($sName)) { + if (!$sNameForTest && !empty($oActions->GetActionParam('create', 0)) && $this->Load($sName)) { throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainAlreadyExists); } return \RainLoop\Model\Domain::fromArray($sNameForTest ?: $sName, [ - 'imapHost' => $oActions->GetActionParam('IncHost', ''), - 'imapPort' => $oActions->GetActionParam('IncPort', 143), - 'imapSecure' => $oActions->GetActionParam('IncSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE), - 'imapShortLogin' => $oActions->GetActionParam('IncShortLogin', 0), - 'useSieve' => $oActions->GetActionParam('UseSieve', 0), - 'sieveHost' => $oActions->GetActionParam('SieveHost', ''), - 'sievePort' => $oActions->GetActionParam('SievePort', 4190), - 'sieveSecure' => $oActions->GetActionParam('SieveSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE), - 'smtpHost' => $oActions->GetActionParam('OutHost', ''), - 'smtpPort' => $oActions->GetActionParam('OutPort', 25), - 'smtpSecure' => $oActions->GetActionParam('OutSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE), - 'smtpShortLogin' => $oActions->GetActionParam('OutShortLogin', 0), - 'smtpAuth' => $oActions->GetActionParam('OutAuth', 1), - 'smtpSetSender' => $oActions->GetActionParam('OutSetSender', 0), - 'smtpPhpMail' => $oActions->GetActionParam('OutUsePhpMail', 0), - 'whiteList' => $oActions->GetActionParam('WhiteList', '') + 'IMAP' => $oActions->GetActionParam('IMAP'), + 'SMTP' => $oActions->GetActionParam('SMTP'), + 'Sieve' => $oActions->GetActionParam('Sieve'), + 'whiteList' => $oActions->GetActionParam('whiteList') ]); } return null; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php index b01d34b2c..7500739e3 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/DefaultDomain.php @@ -135,7 +135,6 @@ class DefaultDomain implements DomainInterface $this->oCacher->Delete(static::wildcardDomainsCacheKey()); } -// \RainLoop\Utils::saveFile($this->sDomainPath.'/'.$sRealFileName.'.ini', $oDomain->ToIniString()); \RainLoop\Utils::saveFile($this->sDomainPath.'/'.$sRealFileName.'.json', \json_encode($oDomain, \JSON_PRETTY_PRINT)); return true; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php index 39aaaad16..c781d06a8 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php @@ -28,12 +28,10 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles public function PutFile(\RainLoop\Model\Account $oAccount, string $sKey, /*resource*/ $rSource) : bool { $bResult = false; - if ($rSource) - { + if ($rSource) { $rOpenOutput = \fopen($this->generateFullFileName($oAccount, $sKey, true), 'w+b'); - if ($rOpenOutput) - { - $bResult = (false !== \MailSo\Base\Utils::MultipleStreamWriter($rSource, array($rOpenOutput))); + if ($rOpenOutput) { + $bResult = (false !== \MailSo\Base\Utils::WriteStream($rSource, $rOpenOutput)); \fclose($rOpenOutput); } } @@ -144,9 +142,9 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); $sFilePath = $this->sDataPath - .'/'.\RainLoop\Utils::fixName($sDomain ?: 'unknown.tld') - .'/'.\RainLoop\Utils::fixName(\implode('@', $aEmail) ?: '.unknown') - .($sSubEmail ? '/'.\RainLoop\Utils::fixName($sSubEmail) : '') + .'/'.\MailSo\Base\Utils::SecureFileName($sDomain ?: 'unknown.tld') + .'/'.\MailSo\Base\Utils::SecureFileName(\implode('@', $aEmail) ?: '.unknown') + .($sSubEmail ? '/'.\MailSo\Base\Utils::SecureFileName($sSubEmail) : '') .'/.files/'.\sha1($sKey); if ($bMkDir && !\is_dir(\dirname($sFilePath)) && !\mkdir(\dirname($sFilePath), 0700, true)) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php index 4b4bc5f2f..afd36bc59 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php @@ -4,12 +4,9 @@ namespace RainLoop\Providers\Filters; class SieveStorage implements FiltersInterface { - const SIEVE_FILE_NAME = 'rainloop.user'; + use \MailSo\Log\Inherit; - /** - * @var \MailSo\Log\Logger - */ - private $oLogger; + const SIEVE_FILE_NAME = 'rainloop.user'; /** * @var \RainLoop\Plugins\Manager @@ -23,18 +20,15 @@ class SieveStorage implements FiltersInterface public function __construct($oPlugins, $oConfig) { - $this->oLogger = null; - $this->oPlugins = $oPlugins; $this->oConfig = $oConfig; } - protected function getConnection(\RainLoop\Model\Account $oAccount) : ?\MailSo\Sieve\ManageSieveClient + protected function getConnection(\RainLoop\Model\Account $oAccount) : ?\MailSo\Sieve\SieveClient { - $oSieveClient = new \MailSo\Sieve\ManageSieveClient(); + $oSieveClient = new \MailSo\Sieve\SieveClient(); $oSieveClient->SetLogger($this->oLogger); - $oSieveClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'sieve_timeout', 10)); - return $oAccount->SieveConnectAndLoginHelper($this->oPlugins, $oSieveClient, $this->oConfig) + return $oAccount->SieveConnectAndLogin($this->oPlugins, $oSieveClient, $this->oConfig) ? $oSieveClient : null; } @@ -67,8 +61,7 @@ class SieveStorage implements FiltersInterface '@Object' => 'Object/SieveScript', 'name' => self::SIEVE_FILE_NAME, 'active' => false, - 'body' => '', - 'filters' => [] + 'body' => '' ); } } @@ -125,9 +118,4 @@ class SieveStorage implements FiltersInterface } return false; } - - public function SetLogger(?\MailSo\Log\Logger $oLogger) - { - $this->oLogger = $oLogger; - } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings.php index d913b36da..b10a50307 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings.php @@ -21,7 +21,7 @@ class Settings extends \RainLoop\Providers\AbstractProvider public function Save(\RainLoop\Model\Account $oAccount, \RainLoop\Settings $oSettings) : bool { - return $this->oDriver->Save($oAccount, $oSettings->DataAsArray()); + return $this->oDriver->Save($oAccount, $oSettings); } public function IsActive() : bool diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php index 53e04b73e..1ea00d7a2 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/DefaultSettings.php @@ -26,11 +26,9 @@ class DefaultSettings implements ISettings self::FILE_NAME ); - if (\is_string($sValue)) - { + if (\is_string($sValue)) { $aData = \json_decode($sValue, true); - if (\is_array($aData)) - { + if (\is_array($aData)) { return $aData; } } @@ -38,14 +36,14 @@ class DefaultSettings implements ISettings return array(); } - public function Save(\RainLoop\Model\Account $oAccount, array $aSettings) : bool + public function Save(\RainLoop\Model\Account $oAccount, \RainLoop\Settings $oSettings) : bool { return $this->oStorageProvider->Put($oAccount, \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, $this->oStorageProvider->IsLocal() ? self::FILE_NAME_LOCAL : self::FILE_NAME, - \json_encode($aSettings)); + \json_encode($oSettings)); } public function Delete(\RainLoop\Model\Account $oAccount) : bool diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php index b1d1a7f8d..0199d1b5e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Settings/ISettings.php @@ -6,7 +6,7 @@ interface ISettings { public function Load(\RainLoop\Model\Account $oAccount) : array; - public function Save(\RainLoop\Model\Account $oAccount, array $aSettings) : bool; + public function Save(\RainLoop\Model\Account $oAccount, \RainLoop\Settings $oSettings) : bool; public function Delete(\RainLoop\Model\Account $oAccount) : bool; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php index c51db5ce9..a9369b391 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php @@ -6,6 +6,8 @@ use RainLoop\Providers\Storage\Enumerations\StorageType; class FileStorage implements \RainLoop\Providers\Storage\IStorage { + use \MailSo\Log\Inherit; + /** * @var string */ @@ -16,16 +18,10 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage */ private $bLocal; - /** - * @var \MailSo\Log\Logger - */ - protected $oLogger; - public function __construct(string $sStoragePath, bool $bLocal = false) { $this->sDataPath = \rtrim(\trim($sStoragePath), '\\/'); $this->bLocal = $bLocal; - $this->oLogger = null; } /** @@ -137,9 +133,9 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); $sFilePath = $this->sDataPath - .'/'.\RainLoop\Utils::fixName($sDomain ?: 'unknown.tld') - .'/'.\RainLoop\Utils::fixName(\implode('@', $aEmail) ?: '.unknown') - .'/'.($sSubFolder ? \RainLoop\Utils::fixName($sSubFolder).'/' : ''); + .'/'.\MailSo\Base\Utils::SecureFileName($sDomain ?: 'unknown.tld') + .'/'.\MailSo\Base\Utils::SecureFileName(\implode('@', $aEmail) ?: '.unknown') + .'/'.($sSubFolder ? \MailSo\Base\Utils::SecureFileName($sSubFolder).'/' : ''); break; default: throw new \Exception("Invalid storage type {$iStorageType}"); @@ -163,17 +159,12 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage if (StorageType::NOBODY === $iStorageType) { $sFilePath .= \sha1($sKey ?: \time()); } else { - $sFilePath .= ($sKey ? \RainLoop\Utils::fixName($sKey) : ''); + $sFilePath .= ($sKey ? \MailSo\Base\Utils::SecureFileName($sKey) : ''); } } return $sFilePath; } - public function SetLogger(?\MailSo\Log\Logger $oLogger) - { - $this->oLogger = $oLogger; - } - public function GC() : void { \clearstatcache(); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions.php index 16b980da6..cca87d0f4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Suggestions.php @@ -7,54 +7,75 @@ class Suggestions extends \RainLoop\Providers\AbstractProvider /** * @var \RainLoop\Providers\Suggestions\ISuggestions[] */ - private $aDrivers; + private array $aDrivers = []; /** * @param \RainLoop\Providers\Suggestions\ISuggestions[]|null $aDriver = null */ public function __construct(?array $aDriver = null) { - if (\is_array($aDriver)) - { - $aDriver = \array_filter($aDriver, function ($oDriver) { + if (\is_array($aDriver)) { + $this->aDrivers = \array_filter($aDriver, function ($oDriver) { return $oDriver instanceof \RainLoop\Providers\Suggestions\ISuggestions; }); } - - $this->aDrivers = \is_array($aDriver) && \count($aDriver) ? $aDriver : null; } public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20) : array { - $aSuggestions = array(); - if ($this->IsActive() && \is_array($this->aDrivers) && \strlen($sQuery)) + if (!\strlen($sQuery)) { + return []; + } + + $iLimit = \max(5, (int) $iLimit); + $aResult = []; + + // Address Book + try { - foreach ($this->aDrivers as $oDriver) - { - $aSubs = null; - if ($oDriver) - { - $aSubs = $oDriver->Process($oAccount, $sQuery, $iLimit); - if (\is_array($aSubs) && \count($aSubs)) - { - $aSuggestions = \array_merge($aSuggestions, $aSubs); + $oAddressBookProvider = \RainLoop\Api::Actions()->AddressBookProvider($oAccount); + if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { + $aSuggestions = $oAddressBookProvider->GetSuggestions($sQuery, $iLimit); + foreach ($aSuggestions as $aItem) { + // Unique email address + $sLine = \mb_strtolower($aItem[0]); + if (!isset($aResult[$sLine])) { + $aResult[$sLine] = $aItem; } } } } - - $aResult = \RainLoop\Utils::RemoveSuggestionDuplicates($aSuggestions); - - if ($iLimit < \count($aResult)) + catch (\Throwable $oException) { - $aResult = \array_slice($aResult, 0, $iLimit); + $this->logException($oException); } - return $aResult; + // Extensions/Plugins + foreach ($this->aDrivers as $oDriver) { + if ($oDriver) try { + $aSuggestions = $oDriver->Process($oAccount, $sQuery, $iLimit); + if ($aSuggestions) { + foreach ($aSuggestions as $aItem) { + // Unique email address + $sLine = \mb_strtolower($aItem[0]); + if (!isset($aResult[$sLine])) { + $aResult[$sLine] = $aItem; + } + } + if ($iLimit < \count($aResult)) { + break; + } + } + } catch (\Throwable $oException) { + $this->logException($oException); + } + } + + return \array_slice(\array_values($aResult), 0, $iLimit); } public function IsActive() : bool { - return \is_array($this->aDrivers) && \count($this->aDrivers); + return \count($this->aDrivers); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php index 6dfb9646d..131068985 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Service.php @@ -10,8 +10,7 @@ abstract class Service public static function Handle() : bool { static $bOne = null; - if (null === $bOne) - { + if (null === $bOne) { $bOne = static::RunResult(); } @@ -23,8 +22,7 @@ abstract class Service $oConfig = Api::Config(); $sServer = \trim($oConfig->Get('security', 'custom_server_signature', '')); - if (\strlen($sServer)) - { + if (\strlen($sServer)) { \header('Server: '.$sServer); } @@ -40,10 +38,31 @@ abstract class Service \header('X-XSS-Protection: '.$sXssProtectionOptionsHeader); $oHttp = \MailSo\Base\Http::SingletonInstance(); - if ($oConfig->Get('labs', 'force_https', false) && !$oHttp->IsSecure()) - { - \header('Location: https://'.$oHttp->GetHost(false, false).$oHttp->GetUrl()); - exit; + if ($oConfig->Get('security', 'force_https', false) && !$oHttp->IsSecure()) { + \MailSo\Base\Http::Location('https://'.$oHttp->GetHost(false).$oHttp->GetUrl()); + return true; + } + + // See https://github.com/kjdev/php-ext-brotli + if (!empty($_SERVER['HTTP_ACCEPT_ENCODING']) + && $oConfig->Get('webmail', 'compress_output', false) + && !\ini_get('zlib.output_compression') + && !\ini_get('brotli.output_compression') + ) { + if (\is_callable('brotli_compress_add') && false !== \stripos($_SERVER['HTTP_ACCEPT_ENCODING'], 'br')) { + \ob_start(function(string $buffer, int $phase){ + static $resource; + if ($phase & PHP_OUTPUT_HANDLER_START) { + \header('Content-Encoding: br'); + $resource = \brotli_compress_init(/*int $quality = 11, int $mode = BROTLI_GENERIC*/); + } + return \brotli_compress_add($resource, $buffer, ($phase & PHP_OUTPUT_HANDLER_FINAL) ? BROTLI_FINISH : BROTLI_PROCESS); + }); + } else { + \ob_start('ob_gzhandler'); + } + } else { + \ob_start(); } $sQuery = \trim($_SERVER['QUERY_STRING'] ?? ''); @@ -65,31 +84,25 @@ abstract class Service $aPaths = \explode('/', $sQuery); - $bAdmin = false; - $sAdminPanelHost = $oConfig->Get('security', 'admin_panel_host', ''); - if (empty($sAdminPanelHost)) - { + $sAdminPanelHost = \trim($oConfig->Get('security', 'admin_panel_host', '')); + if (empty($sAdminPanelHost)) { $bAdmin = !empty($aPaths[0]) && ($oConfig->Get('security', 'admin_panel_key', '') ?: 'admin') === $aPaths[0]; $bAdmin && \array_shift($aPaths); - } - else if (empty($aPaths[0]) && \mb_strtolower($sAdminPanelHost) === \mb_strtolower($oHttp->GetHost())) - { - $bAdmin = true; + } else { + $bAdmin = \mb_strtolower($sAdminPanelHost) === \mb_strtolower($oHttp->GetHost()); } $oActions = $bAdmin ? new ActionsAdmin() : Api::Actions(); $oActions->Plugins()->RunHook('filter.http-paths', array(&$aPaths)); - if ($oHttp->IsPost()) - { + if ($oHttp->IsPost()) { $oHttp->ServerNoCache(); } $oServiceActions = new ServiceActions($oHttp, $oActions); - if ($bAdmin && !$oConfig->Get('security', 'allow_admin_panel', true)) - { + if ($bAdmin && !$oConfig->Get('security', 'allow_admin_panel', true)) { \MailSo\Base\Http::StatusHeader(403); echo $oServiceActions->ErrorTemplates('Access Denied.', 'Access to the SnappyMail Admin Panel is not allowed!'); @@ -98,10 +111,8 @@ abstract class Service } $bIndex = true; - $sResult = ''; - if (\count($aPaths) && !empty($aPaths[0]) && 'index' !== \strtolower($aPaths[0])) - { - if ('mailto' !== \strtolower($aPaths[0]) && !\SnappyMail\HTTP\SecFetch::isSameOrigin()) { + if (\count($aPaths) && !empty($aPaths[0]) && 'index' !== \strtolower($aPaths[0])) { + if ('mailto' !== \strtolower($aPaths[0]) && !\SnappyMail\HTTP\SecFetch::matchAnyRule($oConfig->Get('security', 'secfetch_allow', ''))) { \MailSo\Base\Http::StatusHeader(403); echo $oServiceActions->ErrorTemplates('Access Denied.', "Disallowed Sec-Fetch @@ -112,80 +123,85 @@ abstract class Service return false; } - $bIndex = false; $sMethodName = 'Service'.\preg_replace('/@.+$/', '', $aPaths[0]); $sMethodExtra = \strpos($aPaths[0], '@') ? \preg_replace('/^[^@]+@/', '', $aPaths[0]) : ''; - if (\method_exists($oServiceActions, $sMethodName) && - \is_callable(array($oServiceActions, $sMethodName))) - { + if (\method_exists($oServiceActions, $sMethodName) && \is_callable(array($oServiceActions, $sMethodName))) { + $bIndex = false; $oServiceActions->SetQuery($sQuery)->SetPaths($aPaths); - $sResult = $oServiceActions->{$sMethodName}($sMethodExtra); - } - else if (!$oActions->Plugins()->RunAdditionalPart($aPaths[0], $aPaths)) - { - $bIndex = true; + echo $oServiceActions->{$sMethodName}($sMethodExtra); + } else if ($oActions->Plugins()->RunAdditionalPart($aPaths[0], $aPaths)) { + $bIndex = false; } } - if ($bIndex) - { + if ($bIndex) { + // https://github.com/the-djmaze/snappymail/issues/1024 + $oHttp->ServerNoCache(); + if (!$bAdmin) { $login = $oConfig->Get('labs', 'custom_login_link', ''); if ($login && !$oActions->getAccountFromToken(false)) { - \header("Location: {$login}"); - exit; + \MailSo\Base\Http::Location($login); + return true; } } // if (!\SnappyMail\HTTP\SecFetch::isEntering()) { \header('Content-Type: text/html; charset=utf-8'); - $oHttp->ServerNoCache(); - if (!\is_dir(APP_DATA_FOLDER_PATH) || !\is_writable(APP_DATA_FOLDER_PATH)) - { + if (!\is_dir(APP_DATA_FOLDER_PATH) || !\is_writable(APP_DATA_FOLDER_PATH)) { echo $oServiceActions->ErrorTemplates( 'Permission denied!', 'SnappyMail can not access the data folder "'.APP_DATA_FOLDER_PATH.'"' ); - return false; } $sLanguage = $oActions->GetLanguage($bAdmin); - $sAppJsMin = $oConfig->Get('labs', 'use_app_debug_js', false) ? '' : '.min'; - $sAppCssMin = $oConfig->Get('labs', 'use_app_debug_css', false) ? '' : '.min'; + $bAppDebug = $oConfig->Get('debug', 'enable', false); + $sAppJsMin = $bAppDebug || $oConfig->Get('debug', 'javascript', false) ? '' : '.min'; + $sAppCssMin = $bAppDebug || $oConfig->Get('debug', 'css', false) ? '' : '.min'; $sFaviconUrl = (string) $oConfig->Get('webmail', 'favicon_url', ''); - $sFaviconPngLink = $sFaviconUrl ? $sFaviconUrl : Utils::WebStaticPath('apple-touch-icon.png'); + $sFaviconPngLink = $sFaviconUrl ?: Utils::WebStaticPath('apple-touch-icon.png'); $sAppleTouchLink = $sFaviconUrl ? '' : Utils::WebStaticPath('apple-touch-icon.png'); + $oActions = Api::Actions(); + + $sThemeName = $oActions->GetTheme($bAdmin); + $aTemplateParameters = array( + '{{BaseAppThemeName}}' => $sThemeName, '{{BaseAppFaviconPngLinkTag}}' => $sFaviconPngLink ? '' : '', '{{BaseAppFaviconTouchLinkTag}}' => $sAppleTouchLink ? '' : '', - '{{BaseAppMainCssLink}}' => Utils::WebStaticPath('css/'.($bAdmin ? 'admin' : 'app').$sAppCssMin.'.css'), - '{{BaseAppThemeCssLink}}' => $oActions->ThemeLink($bAdmin), '{{BaseAppManifestLink}}' => Utils::WebStaticPath('manifest.json'), + '{{BaseFavIconSvg}}' => $sFaviconUrl ? '' : Utils::WebStaticPath('favicon.svg'), '{{LoadingDescriptionEsc}}' => \htmlspecialchars($oConfig->Get('webmail', 'loading_description', 'SnappyMail'), ENT_QUOTES|ENT_IGNORE, 'UTF-8'), '{{BaseAppAdmin}}' => $bAdmin ? 1 : 0 ); - $sCacheFileName = ''; - if ($oConfig->Get('labs', 'cache_system_data', true)) - { - $sCacheFileName = 'TMPL:' . $sLanguage . \md5( - Utils::jsonEncode(array( - $oConfig->Get('cache', 'index', ''), - $oActions->Plugins()->Hash(), - $sAppJsMin, - $sAppCssMin, - $aTemplateParameters, - APP_VERSION - )) - ); + $sCacheFileName = 'TMPL:' . \sha1( + Utils::jsonEncode(array( + $sLanguage, + $oConfig->Get('cache', 'index', ''), + $oActions->Plugins()->Hash(), + $sAppJsMin, + $sAppCssMin, + $aTemplateParameters, + APP_VERSION + )) + ); + + // https://github.com/the-djmaze/snappymail/issues/1024 +// $oActions->verifyCacheByKey($sCacheFileName); + + if ($oConfig->Get('cache', 'system_data', true)) { $sResult = $oActions->Cacher()->Get($sCacheFileName); + } else { + $sResult = ''; } if ($sResult) { @@ -193,13 +209,15 @@ abstract class Service } else { $aTemplateParameters['{{BaseAppBootCss}}'] = \file_get_contents(APP_VERSION_ROOT_PATH.'static/css/boot'.$sAppCssMin.'.css'); $aTemplateParameters['{{BaseAppBootScript}}'] = \file_get_contents(APP_VERSION_ROOT_PATH.'static/js'.($sAppJsMin ? '/min' : '').'/boot'.$sAppJsMin.'.js'); - $aTemplateParameters['{{BaseAppThemeCss}}'] = \preg_replace( - '/\\s*([:;{},]+)\\s*/s', - '$1', - $oActions->compileCss($oActions->GetTheme($bAdmin), $bAdmin) - ); + $aTemplateParameters['{{BaseAppMainCssLink}}'] = Utils::WebStaticPath('css/'.($bAdmin ? 'admin' : 'app').$sAppCssMin.'.css'); + $aTemplateParameters['{{BaseAppThemeCss}}'] = \preg_replace('/\\s*([:;{},]+)\\s*/s', '$1', $oActions->compileCss($sThemeName, $bAdmin)); $aTemplateParameters['{{BaseLanguage}}'] = $oActions->compileLanguage($sLanguage, $bAdmin); $aTemplateParameters['{{BaseTemplates}}'] = Utils::ClearHtmlOutput($oServiceActions->compileTemplates($bAdmin)); + $aTemplateParameters['{{NO_SCRIPT_DESC}}'] = \nl2br($oActions->StaticI18N('NO_SCRIPT_TITLE') . "\n" . $oActions->StaticI18N('NO_SCRIPT_DESC')); + $aTemplateParameters['{{NO_COOKIE_TITLE}}'] = $oActions->StaticI18N('NO_COOKIE_TITLE'); + $aTemplateParameters['{{NO_COOKIE_DESC}}'] = $oActions->StaticI18N('NO_COOKIE_DESC'); + $aTemplateParameters['{{BAD_BROWSER_TITLE}}'] = $oActions->StaticI18N('BAD_BROWSER_TITLE'); + $aTemplateParameters['{{BAD_BROWSER_DESC}}'] = \nl2br($oActions->StaticI18N('BAD_BROWSER_DESC')); $sResult = Utils::ClearHtmlOutput(\file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Index.html')); $sResult = \strtr($sResult, $aTemplateParameters); if ($sCacheFileName) { @@ -207,6 +225,10 @@ abstract class Service } } + $SameSite = \strtolower($oConfig->Get('security', 'cookie_samesite', 'Strict')); + $Secure = (isset($_SERVER['HTTPS']) || 'none' == $SameSite) ? ';secure' : ''; + $sResult = \str_replace('samesite=strict', "samesite={$SameSite}{$Secure}", $sResult); + $sScriptNonce = \SnappyMail\UUID::generate(); static::setCSP($sScriptNonce); $sResult = \str_replace('nonce=""', 'nonce="'.$sScriptNonce.'"', $sResult); @@ -215,16 +237,15 @@ abstract class Service $sScriptHash = 'sha256-'.\base64_encode(\hash('sha256', $script[1], true)); static::setCSP(null, $sScriptHash); */ - } - else if (!\headers_sent()) - { + // https://github.com/the-djmaze/snappymail/issues/1024 +// $oActions->cacheByKey($sCacheFileName); + + echo $sResult; + unset($sResult); + } else if (!\headers_sent()) { \header('X-XSS-Protection: 1; mode=block'); } - // Output result - echo $sResult; - unset($sResult); - $oActions->BootEnd(); return true; @@ -232,22 +253,6 @@ abstract class Service private static function setCSP(string $sScriptNonce = null) : void { - $CSP = new \SnappyMail\HTTP\CSP(\trim(Api::Config()->Get('security', 'content_security_policy', ''))); - $CSP->report = Api::Config()->Get('security', 'csp_report', false); - $CSP->report_only = Api::Config()->Get('debug', 'enable', false); // '0.0.0' === APP_VERSION -// $CSP->frame = \explode(' ', Api::Config()->Get('security', 'csp_frame', '')); - - // Allow https: due to remote images in e-mails or use proxy - if (!Api::Config()->Get('security', 'use_local_proxy_for_external_images', '')) { - $CSP->img[] = 'https:'; - $CSP->img[] = 'http:'; - } - if ($sScriptNonce) { - $CSP->script[] = "'nonce-{$sScriptNonce}'"; - } - - Api::Actions()->Plugins()->RunHook('main.content-security-policy', array($CSP)); - - $CSP->setHeaders(); + Api::getCSP($sScriptNonce)->setHeaders(); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index e62372347..85aaf1c95 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -4,32 +4,18 @@ namespace RainLoop; class ServiceActions { - /** - * @var \MailSo\Base\Http - */ - protected $oHttp; + protected \MailSo\Base\Http $oHttp; - /** - * @var \RainLoop\Actions - */ - protected $oActions; + protected Actions $oActions; - /** - * @var array - */ - protected $aPaths; + protected array $aPaths = array(); - /** - * @var string - */ - protected $sQuery; + protected string $sQuery = ''; public function __construct(\MailSo\Base\Http $oHttp, Actions $oActions) { $this->oHttp = $oHttp; $this->oActions = $oActions; - $this->aPaths = array(); - $this->sQuery = ''; } private function Logger() : \MailSo\Log\Logger @@ -88,14 +74,15 @@ class ServiceActions { \ob_start(); - $aResponseItem = null; + $aResponse = null; $oException = null; - $_POST = \json_decode(\file_get_contents('php://input'), true); + if (empty($_POST) || (!empty($_SERVER['CONTENT_TYPE']) && \str_contains($_SERVER['CONTENT_TYPE'], 'application/json'))) { + $_POST = \json_decode(\file_get_contents('php://input'), true); + } - $sAction = $_POST['Action'] ?? null; - if (empty($sAction) && $this->oHttp->IsGet() && !empty($this->aPaths[2])) - { + $sAction = $_POST['Action'] ?? ''; + if (empty($sAction) && $this->oHttp->IsGet() && !empty($this->aPaths[2])) { $sAction = $this->aPaths[2]; } @@ -103,68 +90,66 @@ class ServiceActions try { - if ($this->oHttp->IsPost() && - $this->Config()->Get('security', 'csrf_protection', false) && - ($_POST['XToken'] ?? '') !== Utils::GetCsrfToken()) - { - throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'CSRF failed'); + if (empty($sAction)) { + throw new Exceptions\ClientException(Notifications::InvalidInputArgument, null, 'Action unknown'); } - if (!empty($sAction)) - { - if ($this->oActions instanceof ActionsAdmin && 0 === \stripos($sAction, 'Admin') && !\in_array($sAction, ['AdminLogin', 'AdminLogout'])) { - $this->oActions->IsAdminLoggined(); - } - $sMethodName = 'Do'.$sAction; + $xtoken = $token = Utils::GetCsrfToken(); + if (isset($_SERVER['HTTP_X_SM_TOKEN'])) { + $xtoken = $_SERVER['HTTP_X_SM_TOKEN']; + } else if ($this->oHttp->IsPost()) { + $xtoken = $_POST['XToken'] ?? ''; + } + if ($xtoken !== $token) { + throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'Token mismatch'); + } - $this->Logger()->Write('Action: '.$sMethodName, \LOG_INFO, 'JSON'); + if ($this->oActions instanceof ActionsAdmin && 0 === \stripos($sAction, 'Admin') && !\in_array($sAction, ['AdminLogin', 'AdminLogout'])) { + $this->oActions->IsAdminLoggined(); + } - $aPost = $_POST ?? null; - if ($aPost) - { - $this->oActions->SetActionParams($aPost, $sMethodName); - foreach ($aPost as $key => $value) { - if (false !== \stripos($key, 'Password')) { - $aPost[$key] = '*******'; - } + $sMethodName = 'Do'.$sAction; + + $this->Logger()->Write('Action: '.$sMethodName, \LOG_INFO, 'JSON'); + + $aPost = $_POST ?? null; + if ($aPost) { + $this->oActions->SetActionParams($aPost, $sMethodName); + foreach ($aPost as $key => $value) { + if (false !== \stripos($key, 'Password')) { + $aPost[$key] = '*******'; } + } /* - switch ($sMethodName) - { - case 'DoLogin': - case 'DoAdminLogin': - case 'DoAccountAdd': - $this->Logger()->AddSecret($this->oActions->GetActionParam('Password', '')); - break; - } + switch ($sMethodName) + { + case 'DoLogin': + case 'DoAdminLogin': + case 'DoAccountAdd': + $this->Logger()->AddSecret($this->oActions->GetActionParam('Password', '')); + break; + } */ - $this->Logger()->Write(Utils::jsonEncode($aPost), - \LOG_INFO, 'POST', true); - } - else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) - { - $this->oActions->SetActionParams(array( - 'RawKey' => empty($this->aPaths[3]) ? '' : $this->aPaths[3] - ), $sMethodName); - } - - if (\method_exists($this->oActions, $sMethodName) && - \is_callable(array($this->oActions, $sMethodName))) - { - $this->Plugins()->RunHook('json.action-pre-call', array($sAction)); - $aResponseItem = $this->oActions->{$sMethodName}(); - $this->Plugins()->RunHook('json.action-post-call', array($sAction, &$aResponseItem)); - } - else if ($this->Plugins()->HasAdditionalJson($sMethodName)) - { - $this->Plugins()->RunHook('json.action-pre-call', array($sAction)); - $aResponseItem = $this->Plugins()->RunAdditionalJson($sMethodName); - $this->Plugins()->RunHook('json.action-post-call', array($sAction, &$aResponseItem)); - } + $this->Logger()->Write(Utils::jsonEncode($aPost), \LOG_INFO, 'POST', true); + } else if (3 < \count($this->aPaths) && $this->oHttp->IsGet()) { + $this->oActions->SetActionParams(array( + 'RawKey' => empty($this->aPaths[3]) ? '' : $this->aPaths[3] + ), $sMethodName); } - if (!\is_array($aResponseItem)) - { + if (\method_exists($this->oActions, $sMethodName) && \is_callable(array($this->oActions, $sMethodName))) { + $this->Plugins()->RunHook("json.before-{$sAction}"); + $aResponse = $this->oActions->{$sMethodName}(); + } else if ($this->Plugins()->HasAdditionalJson($sMethodName)) { + $this->Plugins()->RunHook("json.before-{$sAction}"); + $aResponse = $this->Plugins()->RunAdditionalJson($sMethodName); + } + + if (\is_array($aResponse)) { + $this->Plugins()->RunHook("json.after-{$sAction}", array(&$aResponse)); + } + + if (!\is_array($aResponse)) { throw new Exceptions\ClientException(Notifications::UnknownError); } } @@ -175,34 +160,32 @@ class ServiceActions \SnappyMail\Log::warning('SERVICE', "- {$e->getMessage()} @ {$e->getFile()}#{$e->getLine()}"); } - $aResponseItem = $this->oActions->ExceptionResponse( - empty($sAction) ? 'Unknown' : $sAction, $oException); + $aResponse = $this->oActions->ExceptionResponse($oException); } - if (\is_array($aResponseItem)) - { - $aResponseItem['Time'] = (int) ((\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000); - } - - $this->Plugins()->RunHook('filter.json-response', array($sAction, &$aResponseItem)); + $aResponse['Action'] = $sAction ?: 'Unknown'; if (!\headers_sent()) { \header('Content-Type: application/json; charset=utf-8'); } - $sResult = Utils::jsonEncode($aResponseItem); + if (\is_array($aResponse)) { + $aResponse['epoch'] = \time(); +// if ($this->Config()->Get('debug', 'enable', false)) { +// $aResponse['rtime'] = \round(\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 3); +// } + } + + $sResult = Utils::jsonEncode($aResponse); $sObResult = \ob_get_clean(); - if ($this->Logger()->IsEnabled()) - { - if (\strlen($sObResult)) - { + if ($this->Logger()->IsEnabled()) { + if (\strlen($sObResult)) { $this->Logger()->Write($sObResult, \LOG_ERR, 'OB-DATA'); } - if ($oException) - { + if ($oException) { $this->Logger()->WriteException($oException, \LOG_ERR); } @@ -214,114 +197,59 @@ class ServiceActions return $sResult; } - public function ServiceAppend() : string - { - \ob_start(); - $bResponse = false; - $oException = null; - try - { - if (\method_exists($this->oActions, 'Append') && - \is_callable(array($this->oActions, 'Append'))) - { - isset($_POST) && $this->oActions->SetActionParams($_POST, 'Append'); - $bResponse = $this->oActions->Append(); - } - } - catch (\Throwable $oException) - { - $bResponse = false; - } - - \header('Content-Type: text/plain; charset=utf-8'); - $sResult = true === $bResponse ? '1' : '0'; - - $sObResult = \ob_get_clean(); - if (\strlen($sObResult)) - { - $this->Logger()->Write($sObResult, \LOG_ERR, 'OB-DATA'); - } - - if ($oException) - { - $this->Logger()->WriteException($oException, \LOG_ERR); - } - - $this->Logger()->Write($sResult, \LOG_INFO, 'APPEND'); - - return $sResult; - } - private function privateUpload(string $sAction, int $iSizeLimit = 0) : string { $oConfig = $this->Config(); \ob_start(); - $aResponseItem = null; + $aResponse = null; try { $aFile = null; $sInputName = 'uploader'; - $iError = Enumerations\UploadError::UNKNOWN; $iSizeLimit = (0 < $iSizeLimit ? $iSizeLimit : ((int) $oConfig->Get('webmail', 'attachment_size_limit', 0))) * 1024 * 1024; - $iError = UPLOAD_ERR_OK; $_FILES = isset($_FILES) ? $_FILES : null; - if (isset($_FILES[$sInputName], $_FILES[$sInputName]['name'], $_FILES[$sInputName]['tmp_name'], $_FILES[$sInputName]['size'])) - { + if (isset($_FILES[$sInputName], $_FILES[$sInputName]['name'], $_FILES[$sInputName]['tmp_name'], $_FILES[$sInputName]['size'])) { $iError = (isset($_FILES[$sInputName]['error'])) ? (int) $_FILES[$sInputName]['error'] : UPLOAD_ERR_OK; // \is_uploaded_file($_FILES[$sInputName]['tmp_name']) - if (UPLOAD_ERR_OK === $iError && 0 < $iSizeLimit && $iSizeLimit < (int) $_FILES[$sInputName]['size']) - { + if (UPLOAD_ERR_OK === $iError && 0 < $iSizeLimit && $iSizeLimit < (int) $_FILES[$sInputName]['size']) { $iError = Enumerations\UploadError::CONFIG_SIZE; } - if (UPLOAD_ERR_OK === $iError) - { + if (UPLOAD_ERR_OK === $iError) { $aFile = $_FILES[$sInputName]; } - } - else if (empty($_FILES)) - { + } else if (empty($_FILES)) { $iError = UPLOAD_ERR_INI_SIZE; - } - else - { + } else { $iError = Enumerations\UploadError::EMPTY_FILES_DATA; } - if (\method_exists($this->oActions, $sAction) && - \is_callable(array($this->oActions, $sAction))) - { - $aActionParams = isset($_GET) && \is_array($_GET) ? $_GET : null; - - $aActionParams['File'] = $aFile; - $aActionParams['Error'] = $iError; - - $this->oActions->SetActionParams($aActionParams, $sAction); - - $aResponseItem = $this->oActions->{$sAction}(); + if (\method_exists($this->oActions, $sAction) && \is_callable(array($this->oActions, $sAction))) { + $aResponse = $this->oActions->{$sAction}($aFile, $iError); } - if (!is_array($aResponseItem)) - { + if (!is_array($aResponse)) { throw new Exceptions\ClientException(Notifications::UnknownError); } + + $this->Plugins()->RunHook('filter.upload-response', array(&$aResponse)); } catch (\Throwable $oException) { - $aResponseItem = $this->oActions->ExceptionResponse($sAction, $oException); + $aResponse = $this->oActions->ExceptionResponse($oException); } + $aResponse['Action'] = $sAction ?: 'Unknown'; + \header('Content-Type: application/json; charset=utf-8'); - $this->Plugins()->RunHook('filter.upload-response', array(&$aResponseItem)); - $sResult = Utils::jsonEncode($aResponseItem); + $sResult = Utils::jsonEncode($aResponse); $sObResult = \ob_get_clean(); - if (\strlen($sObResult)) - { + if (\strlen($sObResult)) { $this->Logger()->Write($sObResult, \LOG_ERR, 'OB-DATA'); } @@ -358,21 +286,25 @@ class ServiceActions $HTTP->max_redirects = 2; $HTTP->streamBodyTo($tmp); $oResponse = $HTTP->doRequest('GET', $sUrl); - if ($oResponse && 200 === $oResponse->status - && \str_starts_with($oResponse->getHeader('content-type'), 'image/') - ) try { - $this->oActions->cacheByKey($sData); - \header('Content-Type: ' . $oResponse->getHeader('content-type')); - \header('Cache-Control: public'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', 2592000 + \time()).' UTC'); - \header('X-Content-Redirect-Location: '.$oResponse->final_uri); - \rewind($tmp); - \fpassthru($tmp); - exit; - } catch (\Throwable $e) { - $msg = \get_class($HTTP) . ': ' . $e->getMessage(); - \SnappyMail\Log::error('Proxy', $msg); -// \error_log(\get_class($HTTP) . ': ' . $e->getMessage()); + if ($oResponse) { + $sContentType = \SnappyMail\File\MimeType::fromStream($tmp) ?: $oResponse->getHeader('content-type'); + if (200 === $oResponse->status && \str_starts_with($sContentType, 'image/')) { + try { + $this->oActions->cacheByKey($sData); + \header('Content-Type: ' . $sContentType); + \header('Cache-Control: public'); + \header('Expires: '.\gmdate('D, j M Y H:i:s', 2592000 + \time()).' UTC'); + \header('X-Content-Redirect-Location: '.$oResponse->final_uri); + \rewind($tmp); + \fpassthru($tmp); + exit; + } catch (\Throwable $e) { + \header("X-Content-Error: {$e->getMessage()}"); + \SnappyMail\Log::error('Proxy', \get_class($HTTP) . ': ' . $e->getMessage()); + } + } else { + \header("X-Content-Error: {$oResponse->status} {$sContentType}"); + } } } } @@ -396,12 +328,10 @@ class ServiceActions try { $sRawError = 'Invalid action'; - if (\strlen($sAction)) - { + if (\strlen($sAction)) { try { $sMethodName = 'Raw'.$sAction; - if (\method_exists($this->oActions, $sMethodName)) - { + if (\method_exists($this->oActions, $sMethodName)) { \header('X-Raw-Action: '.$sMethodName); \header('Content-Security-Policy: script-src \'none\'; child-src \'none\''); @@ -411,22 +341,17 @@ class ServiceActions 'Params' => $this->aPaths ), $sMethodName); - if (!$this->oActions->{$sMethodName}()) - { + if (!$this->oActions->{$sMethodName}()) { $sRawError = 'False result'; } - } - else - { + } else { $sRawError = 'Unknown action "'.$sAction.'"'; } } catch (\Throwable $e) { // error_log(print_r($e,1)); $sRawError = $e->getMessage(); } - } - else - { + } else { $sRawError = 'Empty action'; } } @@ -441,14 +366,12 @@ class ServiceActions $sRawError = 'Exception as result'; } - if (\strlen($sRawError)) - { + if (\strlen($sRawError)) { $this->Logger()->Write($sRawError, \LOG_ERR); $this->Logger()->WriteDump($this->aPaths, \LOG_ERR, 'PATHS'); } - if ($oException) - { + if ($oException) { $this->Logger()->WriteException($oException, \LOG_ERR, 'RAW'); } @@ -457,42 +380,30 @@ class ServiceActions public function ServiceLang() : string { -// sleep(2); $sResult = ''; \header('Content-Type: application/javascript; charset=utf-8'); - if (!empty($this->aPaths[3])) - { + if (!empty($this->aPaths[3])) { $bAdmin = 'Admin' === (isset($this->aPaths[2]) ? (string) $this->aPaths[2] : 'App'); $sLanguage = $this->oActions->ValidateLanguage($this->aPaths[3], '', $bAdmin); - $bCacheEnabled = $this->Config()->Get('labs', 'cache_system_data', true); - if (!empty($sLanguage) && $bCacheEnabled) - { - $this->oActions->verifyCacheByKey($this->sQuery); - } - + $bCacheEnabled = $this->Config()->Get('cache', 'system_data', true); $sCacheFileName = ''; - if ($bCacheEnabled) - { - $sCacheFileName = KeyPathHelper::LangCache( - $sLanguage, $bAdmin, $this->oActions->Plugins()->Hash()); - + if ($bCacheEnabled) { + $sCacheFileName = KeyPathHelper::LangCache($sLanguage, $bAdmin, $this->oActions->Plugins()->Hash()); + $this->oActions->verifyCacheByKey(\md5($sCacheFileName)); $sResult = $this->Cacher()->Get($sCacheFileName); } - if (!\strlen($sResult)) - { + if (!\strlen($sResult)) { $sResult = $this->oActions->compileLanguage($sLanguage, $bAdmin); - if ($bCacheEnabled && \strlen($sCacheFileName)) - { + if ($sCacheFileName) { $this->Cacher()->Set($sCacheFileName, $sResult); } } - if ($bCacheEnabled) - { - $this->oActions->cacheByKey($this->sQuery); + if ($sCacheFileName) { + $this->oActions->cacheByKey(\md5($sCacheFileName)); } } @@ -506,31 +417,26 @@ class ServiceActions \header('Content-Type: application/javascript; charset=utf-8'); - $bCacheEnabled = $this->Config()->Get('labs', 'cache_system_data', true); - if ($bCacheEnabled) - { - $this->oActions->verifyCacheByKey($this->sQuery); - } + $bAppDebug = $this->Config()->Get('debug', 'enable', false); + $sMinify = ($bAppDebug || $this->Config()->Get('debug', 'javascript', false)) ? '' : 'min'; + $bCacheEnabled = !$bAppDebug && $this->Config()->Get('cache', 'system_data', true); $sCacheFileName = ''; - if ($bCacheEnabled) - { - $sCacheFileName = KeyPathHelper::PluginsJsCache($this->oActions->Plugins()->Hash()); + if ($bCacheEnabled) { + $sCacheFileName = KeyPathHelper::PluginsJsCache($this->oActions->Plugins()->Hash()) . $sMinify; + $this->oActions->verifyCacheByKey(\md5($sCacheFileName)); $sResult = $this->Cacher()->Get($sCacheFileName); } - if (!\strlen($sResult)) - { - $sResult = $this->Plugins()->CompileJs($bAdmin); - if ($bCacheEnabled && \strlen($sCacheFileName)) - { + if (!$sResult) { + $sResult = $this->Plugins()->CompileJs($bAdmin, !!$sMinify); + if ($sCacheFileName) { $this->Cacher()->Set($sCacheFileName, $sResult); } } - if ($bCacheEnabled) - { - $this->oActions->cacheByKey($this->sQuery); + if ($sCacheFileName) { + $this->oActions->cacheByKey(\md5($sCacheFileName)); } return $sResult; @@ -539,45 +445,35 @@ class ServiceActions public function ServiceCss() : string { $sResult = ''; - $bAdmin = !empty($this->aPaths[2]) && 'Admin' === $this->aPaths[2]; $bJson = !empty($this->aPaths[9]) && 'Json' === $this->aPaths[9]; - if ($bJson) - { + if ($bJson) { \header('Content-Type: application/json; charset=utf-8'); - } - else - { + } else { \header('Content-Type: text/css; charset=utf-8'); } $sTheme = ''; - if (!empty($this->aPaths[4])) - { + if (!empty($this->aPaths[4])) { $sTheme = $this->oActions->ValidateTheme($this->aPaths[4]); - $bCacheEnabled = $this->Config()->Get('labs', 'cache_system_data', true); - if ($bCacheEnabled) - { - $this->oActions->verifyCacheByKey($this->sQuery); - } + $bAppDebug = $this->Config()->Get('debug', 'enable', false); + $sMinify = ($bAppDebug || $this->Config()->Get('debug', 'css', false)) ? '' : 'min'; + $bCacheEnabled = !$bAppDebug && $this->Config()->Get('cache', 'system_data', true); $sCacheFileName = ''; - if ($bCacheEnabled) - { - $sCacheFileName = KeyPathHelper::CssCache($sTheme, $this->oActions->Plugins()->Hash()); + if ($bCacheEnabled) { + $sCacheFileName = KeyPathHelper::CssCache($sTheme, $this->oActions->Plugins()->Hash()) . $sMinify; + $this->oActions->verifyCacheByKey(\md5($sCacheFileName . ($bJson ? 1 : 0))); $sResult = $this->Cacher()->Get($sCacheFileName); } - if (!$sResult) - { + if (!$sResult) { try { $sResult = $this->oActions->compileCss($sTheme, $bAdmin); - - if ($bCacheEnabled && $sCacheFileName) - { + if ($sCacheFileName) { $this->Cacher()->Set($sCacheFileName, $sResult); } } @@ -587,9 +483,8 @@ class ServiceActions } } - if ($bCacheEnabled) - { - $this->oActions->cacheByKey($this->sQuery); + if ($sCacheFileName) { + $this->oActions->cacheByKey(\md5($sCacheFileName . ($bJson ? 1 : 0 ))); } } @@ -606,35 +501,12 @@ class ServiceActions return $this->localAppData(true); } - public function ServiceNoScript() : string - { - return $this->localError($this->oActions->StaticI18N('NO_SCRIPT_TITLE'), $this->oActions->StaticI18N('NO_SCRIPT_DESC')); - } - - public function ServiceNoCookie() : string - { - return $this->localError($this->oActions->StaticI18N('NO_COOKIE_TITLE'), $this->oActions->StaticI18N('NO_COOKIE_DESC')); - } - - public function ServiceBadBrowser() : string - { - $sTitle = $this->oActions->StaticI18N('BAD_BROWSER_TITLE'); - $sDesc = \nl2br($this->oActions->StaticI18N('BAD_BROWSER_DESC')); - - \header('Content-Type: text/html; charset=utf-8'); - return \strtr(\file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/BadBrowser.html'), array( - '{{ErrorTitle}}' => $sTitle, - '{{ErrorHeader}}' => $sTitle, - '{{ErrorDesc}}' => $sDesc - )); - } - public function ServiceMailto() : string { $this->oHttp->ServerNoCache(); $sTo = \trim($_GET['to'] ?? ''); - if (!empty($sTo) && \preg_match('/^mailto:/i', $sTo)) { - Utils::SetCookie( + if (\preg_match('/^mailto:/i', $sTo)) { + \SnappyMail\Cookies::set( Actions::AUTH_MAILTO_TOKEN_KEY, Utils::EncodeKeyValuesQ(array( 'Time' => \microtime(true), @@ -643,7 +515,7 @@ class ServiceActions )) ); } - $this->oActions->Location('./'); + \MailSo\Base\Http::Location('./'); return ''; } @@ -667,13 +539,11 @@ class ServiceActions $oAccount = null; $sSsoHash = $_REQUEST['hash'] ?? ''; - if (!empty($sSsoHash)) - { + if (!empty($sSsoHash)) { $mData = null; $sSsoSubData = $this->Cacher()->Get(KeyPathHelper::SsoCacherKey($sSsoHash)); - if (!empty($sSsoSubData)) - { + if (!empty($sSsoSubData)) { $aData = \SnappyMail\Crypt::DecryptFromJSON($sSsoSubData, $sSsoHash); $this->Cacher()->Delete(KeyPathHelper::SsoCacherKey($sSsoHash)); @@ -691,29 +561,24 @@ class ServiceActions { $oAccount = $this->oActions->LoginProcess($sEmail, $sPassword); - if ($aAdditionalOptions) - { + if ($aAdditionalOptions) { $bNeedToSettings = false; $oSettings = $this->SettingsProvider()->Load($oAccount); - if ($oSettings) - { - $sLanguage = isset($aAdditionalOptions['Language']) ? - $aAdditionalOptions['Language'] : ''; + if ($oSettings) { + $sLanguage = isset($aAdditionalOptions['language']) ? + $aAdditionalOptions['language'] : ''; - if ($sLanguage) - { + if ($sLanguage) { $sLanguage = $this->oActions->ValidateLanguage($sLanguage); - if ($sLanguage !== $oSettings->GetConf('Language', '')) - { + if ($sLanguage !== $oSettings->GetConf('language', '')) { $bNeedToSettings = true; - $oSettings->SetConf('Language', $sLanguage); + $oSettings->SetConf('language', $sLanguage); } } } - if ($bNeedToSettings) - { + if ($bNeedToSettings) { $this->SettingsProvider()->Save($oAccount, $oSettings); } } @@ -730,113 +595,10 @@ class ServiceActions } } - $this->oActions->Location('./'); + \MailSo\Base\Http::Location('./'); return ''; } - public function ServiceRemoteAutoLogin() : string - { - $oException = null; - $oAccount = null; - - $sEmail = $_ENV['REMOTE_USER'] ?? ''; - $sPassword = $_ENV['REMOTE_PASSWORD'] ?? ''; - - if (\strlen($sEmail) && \strlen(\trim($sPassword))) - { - try - { - $oAccount = $this->oActions->LoginProcess($sEmail, $sPassword); - if ($oAccount instanceof Model\MainAccount) { - $this->oActions->SetAuthToken($oAccount); - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException); - } - } - - $this->oActions->Location('./'); - return ''; - } - - public function ServiceExternalLogin() : string - { - $this->oHttp->ServerNoCache(); - - $oException = null; - $oAccount = null; - - if ($this->oActions->Config()->Get('labs', 'allow_external_login', false)) { - $sEmail = \trim($_POST['Email']); - $sPassword = $_POST['Password']; - - try - { - $oAccount = $this->oActions->LoginProcess($sEmail, $sPassword); - if ($oAccount instanceof Model\MainAccount) { - $this->oActions->SetAuthToken($oAccount); - } else { - $oAccount = null; - } - } - catch (\Throwable $oException) - { - $this->Logger()->WriteException($oException); - } - } - - if ('json' === \strtolower($_POST['Output'])) { - \header('Content-Type: application/json; charset=utf-8'); - - $aResult = array( - 'Action' => 'ExternalLogin', - 'Result' => $oAccount ? true : false, - 'ErrorCode' => 0 - ); - - if (!$oAccount) { - if ($oException instanceof Exceptions\ClientException) { - $aResult['ErrorCode'] = $oException->getCode(); - } else { - $aResult['ErrorCode'] = Notifications::AuthError; - } - } - - return Utils::jsonEncode($aResult); - } - - $this->oActions->Location('./'); - return ''; - } - - public function ServiceExternalSso() : string - { - $this->oHttp->ServerNoCache(); - $sResult = ''; - $oConfig = $this->oActions->Config(); - $sKey = $oConfig->Get('labs', 'external_sso_key', ''); - $sEmail = \trim($_POST['Email']); - $sPassword = $_POST['Password']; - if ($oConfig->Get('labs', 'allow_external_sso', false) - && $sEmail && $sPassword - && $sKey && $_POST['SsoKey'] == $sKey - ) { - $sResult = \RainLoop\Api::CreateUserSsoHash($sEmail, $sPassword); - if ('json' == \strtolower($_POST['Output'] ?? 'Plain')) { - \header('Content-Type: application/json; charset=utf-8'); - $sResult = \json_encode(array( - 'Action' => 'ExternalSso', - 'Result' => $sResult - )); - } else { - \header('Content-Type: text/plain'); - } - } - return $sResult; - } - public function ErrorTemplates(string $sTitle, string $sDesc, bool $bShowBackLink = true) : string { return \strtr(\file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Error.html'), array( @@ -857,18 +619,16 @@ class ServiceActions private function localAppData(bool $bAdmin = false) : string { - \header('Content-Type: application/javascript; charset=utf-8'); + \header('Content-Type: application/json; charset=utf-8'); $this->oHttp->ServerNoCache(); try { - $sResult = 'rl.initData(' - . Utils::jsonEncode($this->oActions->AppData($bAdmin)) - . ');'; - + $sResult = Utils::jsonEncode($this->oActions->AppData($bAdmin)); $this->Logger()->Write($sResult, \LOG_INFO, 'APPDATA'); - return $sResult; - } catch (\Throwable $e) { - return 'alert(' . \json_encode('ERROR: ' . $e->getMessage()) . ');'; + } catch (\Throwable $oException) { + $this->Logger()->WriteExceptionShort($oException); + \MailSo\Base\Http::StatusHeader(500); + return $oException->getMessage(); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php index f1e8c4683..b2791824b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Settings.php @@ -2,23 +2,36 @@ namespace RainLoop; -class Settings +class Settings implements \JsonSerializable { - /** - * @var array - */ - protected $aData = array(); + protected array $aData = array(); public function __construct(array $aData) { + if (isset($aData['SpamFolder']) && !isset($aData['JunkFolder'])) { + $aData['JunkFolder'] = $aData['SpamFolder']; + } + if (isset($aData['DraftFolder']) && !isset($aData['DraftsFolder'])) { + $aData['DraftsFolder'] = $aData['DraftFolder']; + } + if (isset($aData['Language']) && !isset($aData['language'])) { + $aData['language'] = $aData['Language']; + unset($aData['Language']); + } $this->aData = $aData; } - public function DataAsArray() : array + public function toArray() : array { return $this->aData; } + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->aData;; + } + /** * @param mixed $mDefValue = null * diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php index 1d79c5564..6ce00fb6e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php @@ -4,12 +4,6 @@ namespace RainLoop; class Utils { - static $CookieDefaultPath = ''; - - static $CookieSecure = null; - - static $CookieSameSite = 'Strict'; - const /** * 30 days cookie @@ -30,9 +24,11 @@ class Utils public static function jsonEncode($value, int $flags = \JSON_INVALID_UTF8_SUBSTITUTE) : string { try { + /* Issue with \SnappyMail\HTTP\Stream if (Api::Config()->Get('debug', 'enable', false)) { $flags |= \JSON_PRETTY_PRINT; } + */ return \json_encode($value, $flags | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR); } catch (\Throwable $e) { Api::Logger()->WriteException($e, \LOG_ERR, 'JSON'); @@ -58,27 +54,30 @@ class Utils public static function GetSessionToken(bool $generate = true) : ?string { - $sToken = static::GetCookie(self::SESSION_TOKEN, null); + $sToken = \SnappyMail\Cookies::get(self::SESSION_TOKEN); if (!$sToken) { if (!$generate) { return null; } \SnappyMail\Log::debug('TOKENS', 'New SESSION_TOKEN'); $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); - static::SetCookie(self::SESSION_TOKEN, $sToken); + \SnappyMail\Cookies::set(self::SESSION_TOKEN, $sToken); } return \sha1('Session'.APP_SALT.$sToken.'Token'.APP_SALT); } public static function GetConnectionToken() : string { - $sToken = static::GetCookie(self::CONNECTION_TOKEN); - if (!$sToken) - { - $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); - static::SetCookie(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30); + $oActions = \RainLoop\Api::Actions(); + $oAccount = $oActions->getAccountFromToken(false) ?: $oActions->getMainAccountFromToken(false); + if ($oAccount) { + return $oAccount->Hash(); + } + $sToken = \SnappyMail\Cookies::get(self::CONNECTION_TOKEN); + if (!$sToken) { + $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); + \SnappyMail\Cookies::set(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30); } - return \sha1('Connection'.APP_SALT.$sToken.'Token'.APP_SALT); } @@ -89,10 +88,9 @@ class Utils public static function UpdateConnectionToken() : void { - $sToken = static::GetCookie(self::CONNECTION_TOKEN); - if ($sToken) - { - static::SetCookie(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30); + $sToken = \SnappyMail\Cookies::get(self::CONNECTION_TOKEN); + if ($sToken) { + \SnappyMail\Cookies::set(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30); } } @@ -106,89 +104,6 @@ class Utils )); } - /** - * @param mixed $mDefault = null - * @return mixed - */ - public static function GetCookie(string $sName, $mDefault = null) - { - if (isset($_COOKIE[$sName])) { - $aParts = []; - foreach (\array_keys($_COOKIE) as $sCookieName) { - if (\strtok($sCookieName, '~') === $sName) { - $aParts[$sCookieName] = $_COOKIE[$sCookieName]; - } - } - \ksort($aParts); - return \implode('', $aParts); - } - return $mDefault; - } - - public static function GetSecureCookie(string $sName) - { - return isset($_COOKIE[$sName]) - ? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode(static::GetCookie($sName))) - : null; - } - - private static function _SetCookie(string $sName, string $sValue, int $iExpire) - { - $sPath = static::$CookieDefaultPath; - $sPath = $sPath && \strlen($sPath) ? $sPath : '/'; -/* - if (\strlen($sValue) > 4000 - \strlen($sPath . $sName)) { - throw new \Exception("Cookie '{$sName}' value too long"); - } -*/ - if (\strlen($sValue)) { - $_COOKIE[$sName] = $sValue; - } - \setcookie($sName, $sValue, array( - 'expires' => $iExpire, - 'path' => $sPath, -// 'domain' => null, - 'secure' => static::$CookieSecure, - 'httponly' => true, - 'samesite' => static::$CookieSameSite - )); - } - - public static function SetCookie(string $sName, string $sValue = '', int $iExpire = 0) - { - $sPath = static::$CookieDefaultPath; - $sPath = $sPath && \strlen($sPath) ? $sPath : '/'; - // https://github.com/the-djmaze/snappymail/issues/451 - // The 4K browser limit is for the entire cookie, including name, value, expiry date etc. - $iMaxSize = 4000 - \strlen($sPath . $sName); -/* - if ($iMaxSize < \strlen($sValue)) { - throw new \Exception("Cookie '{$sName}' value too long"); - } -*/ - foreach (\str_split($sValue, $iMaxSize) as $i => $sPart) { - static::_SetCookie($i ? "{$sName}~{$i}" : $sName, $sPart, $iExpire); - } - } - - public static function ClearCookie(string $sName) - { - if (isset($_COOKIE[$sName])) { - $sPath = static::$CookieDefaultPath; - foreach (\array_keys($_COOKIE) as $sCookieName) { - if (\strtok($sCookieName, '~') === $sName) { - unset($_COOKIE[$sCookieName]); - static::_SetCookie($sCookieName, '', \time() - 3600 * 24 * 30); - } - } - } - } - - public static function UrlEncode(string $sV, bool $bEncode = false) : string - { - return $bEncode ? \urlencode($sV) : $sV; - } - public static function WebPath() : string { static $sAppPath; @@ -202,7 +117,12 @@ class Utils public static function WebVersionPath() : string { - return self::WebPath() . \str_replace(APP_INDEX_ROOT_PATH, '', APP_VERSION_ROOT_PATH); + return self::WebPath() . 'snappymail/v/' . APP_VERSION . '/'; + /** + * TODO: solve this to support other paths. + * https://github.com/the-djmaze/snappymail/issues/685 + */ +// return self::WebPath() . \str_replace(APP_INDEX_ROOT_PATH, '', APP_VERSION_ROOT_PATH); } public static function WebStaticPath(string $path = '') : string @@ -210,27 +130,15 @@ class Utils return self::WebVersionPath() . 'static/' . $path; } - public static function RemoveSuggestionDuplicates(array $aSuggestions) : array - { - $aResult = array(); - - foreach ($aSuggestions as $aItem) - { - $sLine = \implode('~~', $aItem); - if (!isset($aResult[$sLine])) - { - $aResult[$sLine] = $aItem; - } - } - - return array_values($aResult); - } - public static function inOpenBasedir(string $name) : string { static $open_basedir; if (null === $open_basedir) { - $open_basedir = \array_filter(\explode(PATH_SEPARATOR, \ini_get('open_basedir'))); + $open_basedir = \ini_get('open_basedir'); + if ($open_basedir) { + \SnappyMail\Log::warning('OpenBasedir', "open_basedir restriction in effect. Allowed path(s): {$open_basedir}"); + } + $open_basedir = \array_filter(\explode(PATH_SEPARATOR, $open_basedir)); } if ($open_basedir) { foreach ($open_basedir as $dir) { @@ -238,21 +146,12 @@ class Utils return true; } } - \SnappyMail\Log::warning('OpenBasedir', "open_basedir restriction in effect. {$name} is not within the allowed path(s): " . \ini_get('open_basedir')); +// \SnappyMail\Log::warning('OpenBasedir', "open_basedir restriction in effect. {$name} is not within the allowed path(s): " . \ini_get('open_basedir')); return false; } return true; } - /** - * Replace control characters, ampersand, spaces and reserved characters (based on Win95 VFAT) - * en.wikipedia.org/wiki/Filename#Reserved_characters_and_words - */ - public static function fixName(string $filename) : string - { - return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $filename); - } - public static function saveFile(string $filename, string $data) : void { $dir = \dirname($filename); diff --git a/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php b/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php index 9e3e04bcc..4b27bc70b 100644 --- a/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php +++ b/snappymail/v/0.0.0/app/libraries/lessphp/lessc.php @@ -2,9 +2,6 @@ namespace LessPHP; -class stdClass extends \stdClass {}; -class Exception extends \Exception {}; - /** * lessphp v0.3.9-sm * http://leafo.net/lessphp @@ -1480,7 +1477,7 @@ class lessc { /* environment functions */ protected function makeOutputBlock($type, $selectors = null) { - $b = new stdclass; + $b = new \stdClass; $b->lines = array(); $b->children = array(); $b->selectors = $selectors; @@ -1491,7 +1488,7 @@ class lessc { // the state of execution protected function pushEnv($block = null) { - $e = new stdclass; + $e = new \stdClass; $e->parent = $this->env; $e->store = array(); $e->block = $block; @@ -1595,7 +1592,7 @@ class lessc { if ($this->sourceLoc >= 0) { $this->sourceParser->throwError($msg, $this->sourceLoc); } - throw new exception($msg); + throw new \Exception($msg); } static protected $cssColors = array( @@ -1831,7 +1828,7 @@ class lessc_parser { // TODO report where the block was opened if (!is_null($this->env->parent)) - throw new exception('parse error: unclosed block'); + throw new \Exception('parse error: unclosed block'); return $this->env; } @@ -1962,7 +1959,7 @@ class lessc_parser { if ($this->literal('}', false)) { try { $block = $this->pop(); - } catch (exception $e) { + } catch (\Exception $e) { $this->seek($s); $this->throwError($e->getMessage()); } @@ -2939,13 +2936,13 @@ class lessc_parser { // TODO this depends on $this->count if ($this->peek("(.*?)(\n|$)", $m, $count)) { - throw new exception("$msg: failed at `$m[1]` $loc"); + throw new \Exception("$msg: failed at `$m[1]` $loc"); } - throw new exception("$msg: $loc"); + throw new \Exception("$msg: $loc"); } protected function pushBlock($selectors=null, $type=null) { - $b = new stdclass; + $b = new \stdClass; $b->parent = $this->env; $b->type = $type; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php b/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php new file mode 100644 index 000000000..9c670239e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/cookies.php @@ -0,0 +1,138 @@ +Get('labs', 'cookie_default_path', ''); + static::$SameSite = $oConfig->Get('security', 'cookie_samesite', 'Strict'); + static::$Secure = isset($_SERVER['HTTPS']) + || 'None' == static::$SameSite + || !!$oConfig->Get('labs', 'cookie_default_secure', false); + $bOne = true; + } + return $bOne; + } + + public static function get(string $sName) : ?string + { + if (isset($_COOKIE[$sName])) { + $aParts = []; + foreach (\array_keys($_COOKIE) as $sCookieName) { + if (\strtok($sCookieName, '~') === $sName) { + $aParts[$sCookieName] = $_COOKIE[$sCookieName]; + } + } + \ksort($aParts); + return \implode('', $aParts); + } + return null; + } + + public static function getSecure(string $sName) + { + return isset($_COOKIE[$sName]) + ? Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode(static::get($sName))) + : null; + } + + private static function _set(string $sName, string $sValue, int $iExpire, bool $httponly = true) : bool + { + $sPath = static::$DefaultPath; + $sPath = $sPath && \strlen($sPath) ? $sPath : '/'; +/* + if (\strlen($sValue) > 4000 - \strlen($sPath . $sName)) { + throw new \Exception("Cookie '{$sName}' value too long"); + } +*/ + if (\strlen($sValue)) { + $_COOKIE[$sName] = $sValue; + } else { + if (!isset($_COOKIE[$sName])) { + return true; + } + unset($_COOKIE[$sName]); + $iExpire = \time() - 3600 * 24 * 30; + } + + // Cookie "$sName" has been rejected because it is already expired. + // Happens when \setcookie() sends multiple with the same name (and one is deleted) + // So when previously set, we must delete all 'Set-Cookie' headers and start over + $cookies = []; + $cookie_remove = false; + foreach (\headers_list() as $header) { + if (\preg_match("/Set-Cookie:([^=]+)=/i", $header, $match)) { + if (\trim($match[1]) == $sName) { + $cookie_remove = true; + } else { + $cookies[] = $header; + } + } + } + if ($cookie_remove) { + \header_remove('Set-Cookie'); + foreach ($cookies as $cookie) { + \header($cookie); + } + } + + return \setcookie($sName, $sValue, array( + 'expires' => $iExpire, + 'path' => $sPath, +// 'domain' => null, + 'secure' => static::$Secure, + 'httponly' => $httponly, + 'samesite' => static::$SameSite + )); + } + + /** + * Firefox: Cookie "$sName" has been rejected because it is already expired. + * \header_remove("set-cookie: {$sName}"); + */ + public static function set(string $sName, string $sValue, int $iExpire = 0, bool $httponly = true) : void + { + static::init(); + $sPath = static::$DefaultPath; + $sPath = $sPath && \strlen($sPath) ? $sPath : '/'; + // https://github.com/the-djmaze/snappymail/issues/451 + // The 4K browser limit is for the entire cookie, including name, value, expiry date etc. + $iMaxSize = 4000 - \strlen($sPath . $sName); +/* + if ($iMaxSize < \strlen($sValue)) { + throw new \Exception("Cookie '{$sName}' value too long"); + } +*/ + // Set the new 4K split cookie + foreach (\str_split($sValue, $iMaxSize) as $i => $sPart) { + $sCookieName = $i ? "{$sName}~{$i}" : $sName; + Log::debug('COOKIE', "set {$sCookieName}"); + static::_set($sCookieName, $sPart, $iExpire); + } + // Delete unused old 4K split cookie parts + foreach (\array_keys($_COOKIE) as $sCookieName) { + $aSplit = \explode('~', $sCookieName); + if (isset($aSplit[1]) && $aSplit[0] == $sName && $aSplit[1] > $i) { + Log::debug('COOKIE', "unset {$sCookieName}"); + static::_set($sCookieName, '', 0); + } + } + } + + public static function clear(string $sName) : void + { + static::init(); + static::_set($sName, '', 0); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php index 6cecb4d8b..74246a40a 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php @@ -18,22 +18,25 @@ abstract class Crypt $list = \array_diff($list, \array_map('strtoupper',$list)); $list = \array_filter($list, function($v){ // DES/ECB/bf/rc insecure, GCM/CCM not supported - return !\preg_match('/(^(des|bf|rc))|-(ecb|gcm|ccm)/i', $v); + return !\preg_match('/(^(des|bf|rc))|-(ecb|gcm|ccm|ocb|siv|cts)|wrap/i', $v); }); \natcasesort($list); } return $list; } + public static function cipherSupported(string $cipher) : bool + { + return \in_array($cipher, static::listCiphers()); + } + public static function setCipher(string $cipher) : bool { - if ($cipher) { - $ciphers = static::listCiphers(); - if (\in_array($cipher, $ciphers)) { - static::$cipher = $cipher; - return true; - } + if (static::cipherSupported($cipher)) { + static::$cipher = $cipher; + return true; } + Log::error('Crypt', "OpenSSL no support for cipher '{$cipher}'"); return false; } @@ -42,10 +45,13 @@ abstract class Crypt */ private static function Passphrase(?string $key) : string { - return \sha1( - ($key ?: \preg_replace('/[^a-z]+/i', '', \explode(')', $_SERVER['HTTP_USER_AGENT'])[0])) . APP_SALT, - true - ); + if (!$key) { + if (empty($_COOKIE['smctoken'])) { + throw new \RuntimeException('Missing smctoken'); + } + $key = $_COOKIE['smctoken'] . APP_VERSION; + } + return \sha1($key . APP_SALT, true); } public static function Decrypt(array $data, string $key = null) /* : mixed */ @@ -55,22 +61,24 @@ abstract class Crypt $fn = "{$data[0]}Decrypt"; if (\method_exists(__CLASS__, $fn)) { $result = static::{$fn}($data[2], $data[1], $key); - return \json_decode($result, true); + if (\is_string($result)) { + return static::jsonDecode($result); + } } } catch (\Throwable $e) { - \trigger_error(__CLASS__ . "::{$fn}(): " . $e->getMessage()); + Log::error('Crypt', "{$fn}(): {$e->getMessage()}"); } -// \trigger_error(__CLASS__ . '::Decrypt() invalid $data or $key'); + Log::warning('Crypt', 'Decrypt() invalid $data or $key'); } else { -// \trigger_error(__CLASS__ . '::Decrypt() invalid $data'); + Log::warning('Crypt', 'Decrypt() invalid $data'); } } public static function DecryptFromJSON(string $data, string $key = null) /* : mixed */ { - $data = \json_decode($data, true); + $data = static::jsonDecode($data); if (!\is_array($data)) { -// \trigger_error(__CLASS__ . '::DecryptFromJSON() invalid $data'); + Log::notice('Crypt', 'DecryptFromJSON() invalid $data'); return null; } return static::Decrypt(\array_map('base64_decode', $data), $key); @@ -80,7 +88,7 @@ abstract class Crypt { $data = \explode('.', $data); if (!\is_array($data)) { -// \trigger_error(__CLASS__ . '::DecryptUrlSafe() invalid $data'); + Log::notice('Crypt', 'DecryptUrlSafe() invalid $data'); return null; } return static::Decrypt(\array_map('MailSo\\Base\\Utils::UrlSafeBase64Decode', $data), $key); @@ -89,22 +97,34 @@ abstract class Crypt public static function Encrypt($data, string $key = null) : array { $data = \json_encode($data); - if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { - $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); - $result = ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)]; - } else if (static::$cipher && \is_callable('openssl_encrypt')) { - $iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher)); - $result = ['openssl', $iv, static::OpenSSLEncrypt($data, $iv, $key)]; - } else { - $salt = \random_bytes(16); - $result = ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)]; + + if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) { + try { + $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); + return ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)]; + } catch (\Throwable $e) { + Log::error('Crypt', 'Sodium ' . $e->getMessage()); + } } + + // Too much OpenSSL v3 issues ? +// if (\is_callable('openssl_encrypt') && OPENSSL_VERSION_NUMBER < 805306368) { + if (\is_callable('openssl_encrypt')) { + try { + $iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher)); + return ['openssl', $iv, static::OpenSSLEncrypt($data, $iv, $key)]; + } catch (\Throwable $e) { + Log::error('Crypt', 'OpenSSL ' . $e->getMessage()); + } + } + + $salt = \random_bytes(16); + return ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)]; /* if (static::{"{$result[0]}Decrypt"}($result[2], $result[1], $key) !== $data) { - throw new \Exception('Encrypt/Decrypt mismatch'); + throw new \RuntimeException('Encrypt/Decrypt mismatch'); } */ - return $result; } public static function EncryptToJSON($data, string $key = null) : string @@ -117,10 +137,10 @@ abstract class Crypt return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key))); } - public static function SodiumDecrypt(string $data, string $nonce, string $key = null) /* : mixed */ + public static function SodiumDecrypt(string $data, string $nonce, string $key = null) /* : string|false */ { if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) { - return null; + throw new \Exception('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt not callable'); } return \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $data, @@ -130,24 +150,35 @@ abstract class Crypt ); } - public static function SodiumEncrypt(string $data, string $nonce, string $key = null) : ?string + public static function SodiumEncrypt(string $data, string $nonce, string $key = null) : string { if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { - return null; + throw new \Exception('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt not callable'); } - return \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( + $result = \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( $data, APP_SALT, $nonce, \str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) ); + if (!$result) { + throw new \RuntimeException('Sodium encryption failed'); + } + return $result; } - public static function OpenSSLDecrypt(string $data, string $iv, string $key = null) /* : mixed */ + public static function OpenSSLDecrypt(string $data, string $iv, string $key = null) /* : string|false */ { - if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_decrypt')) { - return null; + if (!$data || !$iv) { + throw new \InvalidArgumentException('$data or $iv is empty string'); } + if (!\is_callable('openssl_decrypt')) { + throw new \Exception('openssl_decrypt not callable'); + } + if (!static::$cipher) { + throw new \RuntimeException('openssl $cipher not set'); + } + Log::debug('Crypt', 'openssl_decrypt() with cipher ' . static::$cipher); return \openssl_decrypt( $data, static::$cipher, @@ -157,24 +188,35 @@ abstract class Crypt ); } - public static function OpenSSLEncrypt(string $data, string $iv, string $key = null) : ?string + public static function OpenSSLEncrypt(string $data, string $iv, string $key = null) : string { - if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_encrypt')) { - return null; + if (!$data || !$iv) { + throw new \InvalidArgumentException('$data or $iv is empty string'); } - return \openssl_encrypt( + if (!\is_callable('openssl_encrypt')) { + throw new \Exception('openssl_encrypt not callable'); + } + if (!static::$cipher) { + throw new \RuntimeException('openssl $cipher not set'); + } + Log::debug('Crypt', 'openssl_encrypt() with cipher ' . static::$cipher); + $result = \openssl_encrypt( $data, static::$cipher, static::Passphrase($key), OPENSSL_RAW_DATA, $iv ); + if (!$result) { + throw new \RuntimeException('OpenSSL encryption with ' . static::$cipher . ' failed'); + } + return $result; } public static function XxteaDecrypt(string $data, string $salt, string $key = null) /* : mixed */ { if (!$data || !$salt) { - return null; + throw new \InvalidArgumentException('$data or $salt is empty string'); } $key = $salt . static::Passphrase($key); return \is_callable('xxtea_decrypt') @@ -182,19 +224,26 @@ abstract class Crypt : \MailSo\Base\Xxtea::decrypt($data, $key); } - public static function XxteaEncrypt(string $data, string $salt, string $key = null) : ?string + public static function XxteaEncrypt(string $data, string $salt, string $key = null) : string { if (!$data || !$salt) { - return null; + throw new \InvalidArgumentException('$data or $salt is empty string'); } $key = $salt . static::Passphrase($key); - return \is_callable('xxtea_encrypt') + $result = \is_callable('xxtea_encrypt') ? \xxtea_encrypt($data, $key) : \MailSo\Base\Xxtea::encrypt($data, $key); + if (!$result) { + throw new \RuntimeException('Xxtea encryption failed'); + } + return $result; + } + + private static function jsonDecode(string $data) /*: mixed*/ + { + return \json_decode($data, true, 512, JSON_THROW_ON_ERROR); } } -\SnappyMail\Crypt::setCipher(\RainLoop\Api::Config()->Get('security', 'encrypt_cipher', 'aes-256-cbc-hmac-sha1')) - || \SnappyMail\Crypt::setCipher('aes-256-cbc-hmac-sha1') - || \SnappyMail\Crypt::setCipher('aes-256-xts'); +\SnappyMail\Crypt::setCipher(\RainLoop\Api::Config()->Get('security', 'encrypt_cipher', 'aes-256-cbc-hmac-sha1')); 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 b2b62b25d..c3e867dd6 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 @@ -38,10 +38,14 @@ class Client $this->HTTP = \SnappyMail\HTTP\Request::factory(/*'socket'*/); $this->HTTP->proxy = $settings['proxy'] ?? null; - $this->HTTP->setAuth(3, $settings['userName'] ?? '', $settings['password'] ?? ''); + if (!empty($settings['userName']) && !empty($settings['password'])) { + $this->HTTP->setAuth(3, $settings['userName'], $settings['password']); + } else { + \SnappyMail\Log::warning('DAV', 'No user credentials set'); + } $this->HTTP->max_response_kb = 0; $this->HTTP->timeout = 15; // timeout in seconds. -// $this->HTTP->max_redirects = 0; + $this->HTTP->max_redirects = 0; } /** @@ -62,18 +66,19 @@ class Client if (!\preg_match('@^(https?:)?//@', $url)) { // If the url starts with a slash, we must calculate the url based off // the root of the base url. - if (0 === \strpos($url, '/')) { + if (\str_starts_with($url, '/')) { $parts = \parse_url($this->baseUri); $url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url; } else { $url = $this->baseUri . $url; } } + \SnappyMail\Log::debug('DAV', "{$method} {$url}" . ($body ? "\n\t" . \str_replace("\n", "\n\t", $body) : '')); $response = $this->HTTP->doRequest($method, $url, $body, $headers); if (301 == $response->status) { // Like: RewriteRule ^\.well-known/carddav /nextcloud/remote.php/dav [R=301,L] $location = $response->getRedirectLocation(); - \trigger_error("Redirect {$url} to {$location}"); + \SnappyMail\Log::info('DAV', "301 Redirect {$url} to {$location}"); $url = \preg_replace('@^(https?:)?//[^/]+[/$]@', '/', $location); $parts = \parse_url($this->baseUri); $url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url; @@ -82,6 +87,7 @@ class Client if (300 <= $response->status) { throw new \SnappyMail\HTTP\Exception("{$method} {$url}", $response->status, $response); } + \SnappyMail\Log::debug('DAV', "{$response->status}: {$response->body}"); return $response; } @@ -104,23 +110,20 @@ class Client */ public function propFind(string $url, array $properties, int $depth = 0) : array { - $body = '' . "\n"; - $body.= '' . "\n"; - $body.= ' ' . "\n"; + $body = '' . "\n" . ''; foreach ($properties as $property) { if (!\preg_match('/^{([^}]*)}(.*)$/', $property, $match)) { throw new \InvalidArgumentException('\'' . $property . '\' is not a valid clark-notation formatted string'); } if ('DAV:' === $match[1]) { - $body .= " \n"; + $body .= ""; } else { - $body .= " \n"; + $body .= ""; } } - $body .= ' ' . "\n"; - $body .= ''; + $body .= ''; $response = $this->request('PROPFIND', $url, $body, array( "Depth: {$depth}", @@ -144,7 +147,7 @@ class Client null, LIBXML_NOBLANKS | LIBXML_NOCDATA); if (false === $responseXML) { - throw new \InvalidArgumentException('The passed data is not valid XML'); + throw new \UnexpectedValueException("The passed data is not valid XML\n{$response->body}"); } $ns = \array_search('urn:DAV', $responseXML->getNamespaces(true)) ?: 'd'; @@ -163,13 +166,18 @@ class Client foreach ($propStat->xpath("{$ns}:prop") as $prop) { foreach ($prop->xpath("*") as $element) { $propertyName = self::toClarkNotation($element); + $propList[$propertyName] = []; if ('{DAV:}resourcetype' === $propertyName) { - $propList[$propertyName] = []; foreach ($element->xpath("*") as $resourcetype) { $propList[$propertyName][] = self::toClarkNotation($resourcetype); } } else { - $propList[$propertyName] = (string) $element; + foreach ($element->xpath("*") as $child) { + $propList[$propertyName][self::toClarkNotation($child)] = (string) $child; + } + if (!$propList[$propertyName]) { + $propList[$propertyName] = (string) $element; + } } } } 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 302f2e841..cb3ebda20 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/dns.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/dns.php @@ -4,6 +4,10 @@ namespace SnappyMail; abstract class DNS { + /** + * $domain = 'bimigroup.org' + * Then a TXT lookup is done on 'default._bimi.bimigroup.org' + */ public static function BIMI(string $domain) : string { $oCache = \RainLoop\Api::Actions()->Cacher(); @@ -18,7 +22,7 @@ abstract class DNS } if (null === $BIMI) { $BIMI = ''; - $values = \dns_get_record($domain, \DNS_TXT); + $values = \dns_get_record("default._bimi.{$domain}", \DNS_TXT); if ($values) { foreach ($values as $value) { if (\str_starts_with($value['txt'], 'v=BIMI1')) { diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/file/magic.mime.php b/snappymail/v/0.0.0/app/libraries/snappymail/file/magic.mime.php new file mode 100644 index 000000000..2b79d0069 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/magic.mime.php @@ -0,0 +1,34 @@ + 'application/x-7z-compressed', # unknown by magic.mime + '#^BZh.*#s' => 'application/x-bzip2', + '#^\x1f\x8b.*#s' => 'application/x-gzip', + '#^.{257}ustar \x00.*#s' => 'application/x-gtar', + '#^.{257}ustar\x00.*#s' => 'application/x-tar', + '#^Rar!.*#s' => 'application/x-rar-compressed', + '#^PK\x03\x04.*#s' => 'application/zip', + + '#^%PDF-.*#s' => 'application/pdf', + '#^[CF]WS.*#s' => 'application/x-shockwave-flash', + + '#^(.*\n)?(INSERT|CREATE|DROP|DELETE|ALTER|UPDATE)\ .*#is' => 'text/x-sql', + '#^BEGIN:VCARD#s' => 'text/x-vcard', + + '#^GIF8.*#s' => 'image/gif', + '#^.{8}heic#s' => 'image/heic', // https://nokiatech.github.io/heif/technical.html + '#^\xFF\xD8.*#s' => 'image/jpeg', + '#^\x89PNG.*#s' => 'image/png', + '#^.PNG.*#s' => 'image/png', + '#^ 'image/svg+xml', # wild guess, unknown by magic.mime + '#^RIFF.{4}WEBP#s' => 'image/webp', + + '#^FLV.*#s' => 'video/x-flv', + '#^OggS.+\x80theora.*#s' => 'video/ogg', + + '#^ID3.*#s' => 'audio/mpeg', # wild guess? + '#^\xff\xfa.*#s' => 'audio/mpeg', # wild guess? + '#^OggS.+\x01vorbis.*#s' => 'audio/ogg', + + '#^\xD0\xCF\x11\xE0\xA1.*#s' => 'application/msword', + '#^OggS.*#s' => 'application/ogg', +); 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 new file mode 100644 index 000000000..8d41bf681 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/mimetype.php @@ -0,0 +1,278 @@ +file($filename)); + } + if (!$mime && $fp = \fopen($filename, 'rb')) { + $mime = self::fromStream($fp); + \fclose($fp); + } + if ('application/zip' === \str_replace('/x-', '/', $mime)) { + $zip = new \ZipArchive(); + if ($zip->open($filename, \ZIPARCHIVE::RDONLY)) { + if (false !== $zip->locateName('word/_rels/document.xml.rels')) { + return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + } + if (false !== $zip->locateName('xl/_rels/workbook.xml.rels')) { + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + } + } + } + return $mime ? static::detectDeeper($mime, $name ?: $filename) : null; + } + + public static function fromStream($stream, string $name = '') : ?string + { + if (\is_resource($stream) && \stream_get_meta_data($stream)['seekable']) { + $pos = \ftell($stream); +// if (\is_int($pos) && \rewind($stream)) { + if (\is_int($pos) && 0 === \fseek($stream, 0)) { +// $str = \fread($stream, 265); + $str = \stream_get_contents($stream, 265, 0); + \fseek($stream, $pos); + if ($str) { + return static::fromString($str, $name); + } + } + } + return null; + } + + public static function fromString(string &$str, string $name = '') : ?string + { + static::initFInfo(); + $mime = self::$finfo + ? \preg_replace('#[,;].*#', '', self::$finfo->buffer($str)) + : self::getFromData($str); + return $mime ? static::detectDeeper($mime, $name) : null; + } + + protected static function getFromData(string $str) : ?string + { + if (\str_contains($str, '-----BEGIN PGP SIGNATURE-----')) { + return 'application/pgp-signature'; + } + if (\preg_match('/-----BEGIN PGP (PUBLIC|PRIVATE) KEY BLOCK-----/', $str)) { + return 'application/pgp-keys'; + } + static $magic; + if (!$magic) { + require __DIR__ . '/magic.mime.php'; + } + $str = \preg_replace(\array_keys($magic), \array_values($magic), $str, 1, $c); + return $c ? $str : null; + } + + public static function fromFilename(string $filename) : ?string + { + $filename = \strtolower($filename); + if ('winmail.dat' === $filename) { + return 'application/ms-tnef'; + } + $extension = \explode('.', $filename); + $extension = \array_pop($extension); + return isset(static::$types[$extension]) ? static::$types[$extension] : null; + } + + /** + * Issue with 'text/plain' + */ + public static function toExtension(string $mime, bool $include_dot = true) : ?string + { + $mime = \strtolower($mime); + if ('application/pgp-signature' == $mime || 'application/pgp-keys' == $mime) { + $ext = 'asc'; + } else { + $mime = \str_replace('application/x-tar', 'application/gtar', $mime); + $ext = \array_search($mime, static::$types) + ?: \array_search(\str_replace('/x-', '/', $mime), static::$types) + ?: \array_search(\str_replace('/', '/x-', $mime), static::$types) + ?: 'bin'; + } + return ($include_dot ? '.' : '') . $ext; + } + + protected static $types = [ + '7z' => 'application/x-7z-compressed', + 'ai' => 'application/postscript', +// 'asc' => 'application/pgp-signature', +// 'asc' => 'application/pgp-keys', + 'bat' => 'application/x-msdownload', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'cab' => 'application/vnd.ms-cab-compressed', + 'chm' => 'application/vnd.ms-htmlhelp', + 'com' => 'application/x-msdownload', + 'deb' => 'application/x-debian-package', + 'dll' => 'application/x-msdownload', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'eps' => 'application/postscript', + 'epub' => 'application/epub', + 'exe' => 'application/x-msdownload', + 'gz' => 'application/gzip', + 'gz' => 'application/x-gzip', + 'hlp' => 'application/winhlp', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'msi' => 'application/x-msdownload', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ogx' => 'application/ogg', + 'p10' => 'application/pkcs10', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7s' => 'application/pkcs7-signature', + 'pdf' => 'application/pdf', + 'php' => 'application/x-httpd-php', + 'php' => 'application/x-php', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ps' => 'application/postscript', + 'psd' => 'image/vnd.adobe.photoshop', + 'rar' => 'application/rar-compressed', + 'rar' => 'application/x-rar-compressed', + 'rtf' => 'application/rtf', + 'scr' => 'application/x-msdownload', + 'sql' => 'application/sql', + 'swf' => 'application/shockwave-flash', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/gtar', +// 'tar' => 'application/x-tar', +// 'tgz' => 'application/x-gzip', + 'torrent' => 'application/x-bittorrent', + 'wgt' => 'application/widget', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'zip' => 'application/zip', + + 'aac' => 'audio/aac', + 'aif' => 'audio/aiff', + 'aifc' => 'audio/aiff', + 'aiff' => 'audio/aiff', + 'flac' => 'audio/flac', + 'm3u' => 'audio/x-mpegurl', + 'midi' => 'audio/midi', + 'mp3' => 'audio/mpeg', + 'mp4a' => 'audio/mp4', + 'ogg' => 'audio/ogg', + 'wav' => 'audio/wav', + 'weba' => 'audio/webm', + + 'ttf' => 'font/ttf', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + + 'bmp' => 'image/bmp', + 'cgm' => 'image/cgm', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'gif' => 'image/gif', +// 'heic' => 'image/heic', + 'ico' => 'image/vnd.microsoft.icon', +// 'ico' => 'image/x-icon', + 'ief' => 'image/ief', + 'jpeg' => 'image/jpeg', + 'jfif' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'webp' => 'image/webp', + + 'eml' => 'message/rfc822', + 'mime' => 'message/rfc822', + + 'txt' => 'text/plain', + 'asp' => 'text/asp', + 'cfg' => 'text/plain', + 'conf' => 'text/plain', + 'css' => 'text/css', + 'csv' => 'text/csv', + 'def' => 'text/plain', + 'html' => 'text/html', + 'htm' => 'text/html', + 'ics' => 'text/calendar', + 'ifb' => 'text/calendar', + 'in' => 'text/plain', + 'ini' => 'text/plain', + 'list' => 'text/plain', + 'log' => 'text/plain', + 'pl' => 'text/perl', + 'rtx' => 'text/richtext', + 'text' => 'text/plain', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'xml' => 'text/xml', + + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gpp', + 'asf' => 'video/x-ms-asf', + 'asx' => 'video/x-ms-asf', + 'avi' => 'video/x-msvideo', + 'flv' => 'video/flv', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'jpgv' => 'video/jpgv', + 'm4v' => 'video/x-m4v', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp4' => 'video/mp4', + 'mp4v' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'm1v' => 'video/mpeg', + 'm2v' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'ogv' => 'video/ogg', + 'qt' => 'video/quicktime', + 'webm' => 'video/webm', + 'wm' => 'video/x-ms-wm', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wvx' => 'video/x-ms-wvx', + ]; + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php index 85fd5c23f..146eb3b73 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/csp.php @@ -8,57 +8,69 @@ namespace SnappyMail\HTTP; class CSP { public - $base = ["'self'"], - $default = ["'self'"], - // Knockout.js requires unsafe-inline? - // Knockout.js requires eval() for observable binding purposes - $script = ["'self'", "'unsafe-eval'"/*, "'unsafe-inline'"*/], - $img = ["'self'", 'data:'], - $style = ["'self'", "'unsafe-inline'"], - $frame = [], - $report = false, $report_to = [], $report_only = false; + private $directives = [ + 'base-uri' => ["'self'"], + 'default-src' => ["'self'", 'data:'], + // Knockout.js requires eval() for observable binding purposes + // Safari < 15.4 does not support strict-dynamic +// 'script-src' => ["'strict-dynamic'", "'unsafe-eval'"], + 'script-src' => ["'self'", "'unsafe-eval'"], + // Knockout.js requires unsafe-inline? +// 'script-src' => ["'self'", "'unsafe-inline'", "'unsafe-eval'"], + 'img-src' => ["'self'", 'data:'], + 'style-src' => ["'self'", "'unsafe-inline'"], + ]; + function __construct(string $default = '') { if ($default) { foreach (\explode(';', $default) as $directive) { - $values = \explode(' ', $directive); - $name = \preg_replace('/-.+/', '', \trim(\array_shift($values))); - $this->$name = $values; + $sources = \preg_split('/\\s+/', \trim($directive)); + $directive = \array_shift($sources); + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + $this->directives[$directive] = \array_merge($this->directives[$directive], $sources); } } } function __toString() : string { - $params = [ - 'base-uri ' . \implode(' ', $this->base), - 'default-src ' . \implode(' ', $this->default) - ]; - if ($this->script) { - $params[] = 'script-src ' . \implode(' ', $this->script); + // report-uri deprecated + unset($this->directives['report-uri']); + if ($this->report || $this->report_only) { + $this->directives['report-uri'] = [\RainLoop\Utils::WebPath() . '?/CspReport']; } - if ($this->img) { - $params[] = 'img-src ' . \implode(' ', $this->img); + $params = []; + foreach ($this->directives as $directive => $sources) { + $params[] = $directive . ' ' . \implode(' ', \array_unique($sources)); } - if ($this->style) { - $params[] = 'style-src ' . \implode(' ', $this->style); - } - if ($this->frame) { - $params[] = 'frame-src ' . \implode(' ', $this->frame); - } - - // Deprecated - if ($this->report) { - $params[] = 'report-uri ./?/CspReport'; - } - +// if (empty($this->directives['frame-ancestors'])) { +// $params[] = "frame-ancestors 'none';"; +// } return \implode('; ', $params); } + public function add(string $directive, string $source) : void + { + if (!isset($this->directives[$directive])) { + $this->directives[$directive] = []; + } + $this->directives[$directive][] = $source; + } + + public function get(string $directive) : array + { + return isset($this->directives[$directive]) + ? $this->directives[$directive] + : []; + } + public function setHeaders() : void { if ($this->report_only) { @@ -66,6 +78,11 @@ class CSP } else { \header('Content-Security-Policy: ' . $this); } + if (empty($this->directives['frame-ancestors'])) { + \header('X-Frame-Options: DENY'); + } else { +// \header('X-Frame-Options: SAMEORIGIN'); + } } public static function logReport() : void diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php index 406ffc2a9..536317b99 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/secfetch.php @@ -107,11 +107,43 @@ abstract class SecFetch public static function isSameOrigin() : bool { - return !isset($_SERVER['HTTP_SEC_FETCH_SITE']) || 'same-origin' === $_SERVER['HTTP_SEC_FETCH_SITE'] // Fulguris incognito: Dest = document, Mode = navigate, Site = none, User = true || 'none' === $_SERVER['HTTP_SEC_FETCH_SITE']; } + /** + * $rules = Additional allowed Sec-Fetch combinations separated by ";". + * For example: + * Allow iframe on same domain in any mode: dest=iframe,site=same-origin + * Allow navigate to iframe on same domain: mode=navigate,dest=iframe,site=same-origin + * Allow navigate to iframe on (sub)domain: mode=navigate,dest=iframe,site=same-site + * Allow navigate to iframe from any domain: mode=navigate,dest=iframe,site=cross-site + * + * Default is "site=same-origin;site=none" + */ + public static function matchAnyRule(string $rules) : bool + { + if (!isset($_SERVER['HTTP_SEC_FETCH_SITE'])) { + return true; + } + $secfetch = \explode(';', 'site=same-origin;site=none;' . $rules); + foreach ($secfetch as $rule) { + if (\preg_match_all('/(dest|mode|site)=([^,]+)/', $rule, $matches, PREG_SET_ORDER)) { + $data = ['dest'=>'','mode'=>'','site'=>'']; + foreach ($matches as $match) { + $data[$match[1]] = $match[2]; + } + if ((!$data['site'] || static::site($data['site'])) + && (!$data['dest'] || !isset($_SERVER['HTTP_SEC_FETCH_DEST']) || static::dest($data['dest'])) + && (!$data['mode'] || !isset($_SERVER['HTTP_SEC_FETCH_MODE']) || static::mode($data['mode'])) + ) { + return true; + } + } + } + return false; + } + } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/stream.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/stream.php index b01b9beba..4d8435bfb 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/http/stream.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/stream.php @@ -17,8 +17,9 @@ abstract class Stream { \set_time_limit(0); \ob_implicit_flush(); - \ini_set('implicit_flush',1); - \ini_set('output_buffering', 0); + \ini_set('implicit_flush', '1'); + \ini_set('output_buffering', '0'); + \ini_set('display_errors', '0'); if ($i = \ob_get_level()) { # Clear buffers: while ($i-- && \ob_end_clean()); @@ -28,7 +29,7 @@ abstract class Stream // We just fake Drupal https://www.drupal.org/docs/8/core/modules/big-pipe/bigpipe-environment-requirements \header('Surrogate-Control: no-store, content="BigPipe/1.0"'); // Explicitly disable caching so Varnish and other upstreams won't cache. - \header('Cache-Control: no-store, no-cache, must-revalidate'); + \header('Cache-Control: no-store'); \header('Pragma: no-cache'); // Nginx: disable fastcgi_buffering and disable gzip for this request. diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/image/gd2.php b/snappymail/v/0.0.0/app/libraries/snappymail/image/gd2.php index b4dd61aab..3f496b3f8 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/image/gd2.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/image/gd2.php @@ -50,7 +50,8 @@ class GD2 implements \SnappyMail\Image } $gd2 = new static(); $gd2->img = \imagecreatefromstring($data); - if (!\is_resource($gd2->img)) { + // resource or PHP8 GdImage + if (!$gd2->img) { throw new \InvalidArgumentException('Failed to load image'); } $gd2->file = 'blob'; @@ -223,7 +224,7 @@ class GD2 implements \SnappyMail\Image /** rotate clockwise */ if (!\function_exists('imagerotate')) { require __DIR__ . '/gd2/imagerotate.inc'; } $tmp_img = \imagerotate($this->img, $degrees * -1, 0); - if (!\is_resource($tmp_img)) { return false; } + if (!$tmp_img) { return false; } \imagedestroy($this->img); $this->img = $tmp_img; return true; @@ -240,7 +241,7 @@ class GD2 implements \SnappyMail\Image if (!$width || ($bestfit && $tx < $ty)) { $width = \round($x / $ty); } if (!$height || ($bestfit && $tx > $ty)) { $height = \round($y / $tx); } $tmp_img = $this->create_image($width, $height); - if (!\is_resource($tmp_img)) { return false; } + if (!$tmp_img) { return false; } \imagealphablending($tmp_img, false); if (!\imagecopyresampled($tmp_img, $this->img, 0, 0, 0, 0, $width, $height, $x, $y)) { if (!\imagecopyresized($tmp_img, $this->img, 0, 0, 0, 0, $width, $height, $x, $y)) { diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/image/imagick.php b/snappymail/v/0.0.0/app/libraries/snappymail/image/imagick.php index ff9c7807d..bf514fa20 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/image/imagick.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/image/imagick.php @@ -28,12 +28,17 @@ class IMagick extends \Imagick implements \SnappyMail\Image public static function createFromStream($fp) { + // https://github.com/the-djmaze/snappymail/issues/1201 + $data = \stream_get_contents($fp); + return static::createFromString($data); +/* $imagick = new static(); if (!$imagick->readImageFile($fp)) { throw new \InvalidArgumentException('Failed to load image'); } $imagick->setImageMatte(true); return $imagick; +*/ } public function getOrientation() : int diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/imap/sync.php b/snappymail/v/0.0.0/app/libraries/snappymail/imap/sync.php new file mode 100644 index 000000000..d4d0043ba --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/imap/sync.php @@ -0,0 +1,239 @@ +oImapSource->TAG_PREFIX = 'S'; + $this->oImapTarget->TAG_PREFIX = 'T'; + +// $this->oImapTarget->logWrite('Get oImapTarget->FolderList'); + \SnappyMail\Log::notice('SYNC', 'Get oImapTarget->FolderList'); + $aTargetFolders = $this->oImapTarget->FolderList($sParent, $sListPattern); + if (!$aTargetFolders) { + return null; + } + $sTargetDelimiter = ''; + $sTargetRoles = [ + 'inbox' => false, + 'sent' => false, + 'drafts' => false, + 'junk' => false, + 'trash' => false, + 'archive' => false + ]; + foreach ($aTargetFolders as $sFullName => $oImapFolder) { + $role = $oImapFolder->Role(); + if ($role && empty($sTargetRoles[$role])) { + $sTargetRoles[$role] = $sFullName; + } + if (!$sTargetDelimiter) { + $sTargetDelimiter = $oImapFolder->Delimiter(); + } + } + + \SnappyMail\Log::notice('SYNC', 'Get oImapSource->FolderList'); + $bUseListStatus = $this->oImapSource->hasCapability('LIST-EXTENDED'); + $aSourceFolders = $this->oImapSource->FolderList($sParent, $sListPattern, false, $bUseListStatus); + if (!$aSourceFolders) { + return null; + } + + $isCli = false !== \stripos(\php_sapi_name(), 'cli'); + if ($isCli) { + echo 'folders: ' . \count($aSourceFolders) . "\n"; + } else { + \SnappyMail\HTTP\Stream::start(); + \SnappyMail\HTTP\Stream::JSON([ + 'folders' => \count($aSourceFolders) + ]); + } + + $fi = 0; + \ignore_user_abort(true); + foreach ($aSourceFolders as $sSourceFolderName => $oImapFolder) { + ++$fi; + if ($oImapFolder->Selectable()) { + $role = $oImapFolder->Role(); + if ('all' === $role) { + // Don't duplicate all mail + continue; + } + // Detect mailbox name based on role + if (!$sTargetRootFolderName && $role && !empty($sTargetRoles[$role])) { + $sTargetFolderName = $sTargetRoles[$role]; + } + // Else just do a name match + else { + if ($sTargetDelimiter) { + $sTargetFolderName = \str_replace($oImapFolder->Delimiter(), $sTargetDelimiter, $sSourceFolderName); + $sTargetRootFolderName = \str_replace($sTargetDelimiter, '-', $sTargetRootFolderName); + } else { + $sTargetFolderName = $sSourceFolderName; + } + if ($sTargetRootFolderName) { + $sTargetFolderName = $sTargetRootFolderName . ($sTargetDelimiter?:'-') . $sTargetFolderName; + } + } + if ($isCli) { + echo \str_pad($fi, 3, ' ', STR_PAD_LEFT) . " folder: {$sSourceFolderName} => {$sTargetFolderName}\n"; + } else { + \SnappyMail\HTTP\Stream::JSON([ + 'index' => $fi, + 'folder' => $sSourceFolderName, + 'target' => $sTargetFolderName + ]); + } + + // Create mailbox if not exists + if (!isset($aTargetFolders[$sTargetFolderName])) { + $this->oImapTarget->FolderCreate( + $sTargetFolderName, + !$bUseListStatus || $oImapFolder->IsSubscribed() + ); + } else if (!$aTargetFolders[$sTargetFolderName]->Selectable()) { + // Can't copy messages + continue; + } + + // Set Source metadata on target + if ($aMetadata = $oImapFolder->Metadata()) { + $this->oImapTarget->FolderSetMetadata($sTargetFolderName, $aMetadata); + } + + $oSourceInfo = $this->oImapSource->FolderSelect($sSourceFolderName); + if ($oSourceInfo->MESSAGES) { + if ($isCli) { + echo \str_pad($fi, 3, ' ', STR_PAD_LEFT) + . " messages: [" . \str_repeat(' ', 50) . "] 0/{$oSourceInfo->MESSAGES}"; + } else { + \SnappyMail\HTTP\Stream::JSON([ + 'index' => $fi, + 'messages' => $oSourceInfo->MESSAGES + ]); + } + // All id's to skip from source + $oTargetInfo = $this->oImapTarget->FolderSelect($sTargetFolderName); + if ($oTargetInfo->MESSAGES) { + // Get all existing message id's from target to skip + $aTargetMessageIDs = []; + $this->oImapTarget->SendRequest('FETCH', [ + '1:*', [FetchType::BuildBodyCustomHeaderRequest([Header::MESSAGE_ID], true)] + ]); + foreach ($this->oImapTarget->yieldUntaggedResponses() as $oResponse) { + if ('FETCH' === $oResponse->ResponseList[2]) { + // $oResponse->ResponseList[3][0] == 'BODY[HEADER.FIELDS (MESSAGE-ID)]' + // 'Message-ID: ...' + $aTargetMessageIDs[] = $oResponse->ResponseList[3][1]; + } + } + } + // Set all existing id's from source to skip and get all flags + $aSourceSkipIDs = []; + $aSourceFlags = []; + $this->oImapSource->SendRequest('FETCH', [ + '1:*', [FetchType::FLAGS, FetchType::BuildBodyCustomHeaderRequest([Header::MESSAGE_ID], true)] + ]); + foreach ($this->oImapSource->yieldUntaggedResponses() as $oResponse) { + if ('FETCH' === $oResponse->ResponseList[2] + && isset($oResponse->ResponseList[3]) + && \is_array($oResponse->ResponseList[3]) + ) { + $id = $oResponse->ResponseList[1]; + foreach ($oResponse->ResponseList[3] as $i => $mItem) { + if ('FLAGS' === $mItem) { + $aSourceFlags[$id] = $oResponse->ResponseList[3][$i+1]; + } else if ('MESSAGE-ID' === $mItem && \in_array($oResponse->ResponseList[3][$i+1], $aTargetMessageIDs)) { + $aSourceSkipIDs[] = $id; + } + } + } + } + + $aTargetMessageIDs = []; + // Now copy each message from source to target + for ($i = 1; $i <= $oSourceInfo->MESSAGES; ++$i) { + if (!\in_array($i, $aSourceSkipIDs)) { + $sPeek = $this->oImapSource->hasCapability('BINARY') + ? FetchType::BINARY_PEEK + : FetchType::BODY_PEEK; + $iAppendUid = 0; + $aFetchResponse = $this->oImapSource->Fetch(array( + array( + $sPeek.'[]', + function ($sParent, $sLiteralAtomUpperCase, $rLiteralStream, $iLiteralLen) + use ($sTargetFolderName, &$iAppendUid, $aSourceFlags, $i) { + if (\strlen($sLiteralAtomUpperCase) && \is_resource($rLiteralStream) && 'FETCH' === $sParent) { +// $sMessage = \stream_get_contents($rLiteralStream); + $iAppendUid = $this->oImapTarget->MessageAppendStream( + $sTargetFolderName, + $rLiteralStream, + $iLiteralLen, + isset($aSourceFlags[$i]) ? $aSourceFlags[$i] : [] + ); + } + } + )), $i, false); + +/* + $aFlags = $aFetchResponse[0]->GetFetchValue('FLAGS'); + $iAppendUid = $this->oImapTarget->MessageAppendStream( + $sTargetFolderName, + $rLiteralStream, + $iLiteralLen, + $aFlags + ); + if ($iAppendUid && $aFlags) { + $this->MessageStoreFlag( + new SequenceSet([$iAppendUid]), + $aFlags, + \MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT + ); + } +*/ + } + + if ($isCli) { + // Clear line + echo "\x1b[2K\x1b[1G"; + // Echo same line with progress + $p = \floor(50 * $i / $oSourceInfo->MESSAGES); + echo \str_pad($fi, 3, ' ', STR_PAD_LEFT) + . " messages: [" . \str_repeat('=', $p) . \str_repeat(' ', 50 - $p) + . "] {$i}/{$oSourceInfo->MESSAGES}"; + } else { + \SnappyMail\HTTP\Stream::JSON([ + 'index' => $fi, + 'message' => $i + ]); + } + } + if ($isCli) { + echo "\n"; + } + } + } else if ($isCli) { + echo \str_pad($fi, 3, ' ', STR_PAD_LEFT) . " folder: {$sSourceFolderName}\n"; + } else { + \SnappyMail\HTTP\Stream::JSON([ + 'index' => $fi, + 'folder' => $sSourceFolderName + ]); + } + } + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/jwt.php b/snappymail/v/0.0.0/app/libraries/snappymail/jwt.php new file mode 100644 index 000000000..323c2686e --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/jwt.php @@ -0,0 +1,230 @@ +getMessage()})"); + } + try { + $payload = static::jsonDecode($jwt[1]); + } catch (\Throwable $e) { + throw new \UnexpectedValueException("Invalid claims encoding ({$e->getMessage()})"); + } + $sig = Utils::UrlSafeBase64Decode($jwt[2]); + + if (empty($header->alg)) { + throw new \UnexpectedValueException('Empty algorithm'); + } + if (\is_array($key) || $key instanceof \ArrayAccess) { + if (!isset($header->kid)) { + throw new \UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if (!isset($key[$header->kid])) { + throw new \DomainException('No valid key found'); + } + $key = $key[$header->kid]; + } + + // Check the signature + if (!static::verify("{$jwt[0]}.{$jwt[1]}", $sig, $key, $header->alg)) { + throw new \Exception('Signature verification failed'); + } + + // Check if the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && $payload->nbf > ($timestamp + $leeway)) { + throw new \Exception('Cannot handle token prior to ' . \date(\DateTime::ISO8601, $payload->nbf)); + } + + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (isset($payload->iat) && $payload->iat > ($timestamp + $leeway)) { + throw new \Exception('Cannot handle token prior to ' . \date(\DateTime::ISO8601, $payload->iat)); + } + + // Check if this token has expired. + if (isset($payload->exp) && ($timestamp - $leeway) >= $payload->exp) { + throw new \Exception('Expired token'); + } + + return $payload; + } + + /** + * Converts and signs a PHP object or array into a JWT string. + * + * @param object|array $payload PHP object or array + * @param string $key The private/secret key. + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * @param mixed $keyId + * @param array $head An array with header elements to attach + * + * @return string A signed JWT + */ + public static function encode($payload, string $key, string $alg = 'HS256', $keyId = null, array $header = array()) : string + { + $header = \array_merge($header, array('typ' => 'JWT', 'alg' => $alg)); + if ($keyId) { + $header['kid'] = $keyId; + } + $segments = array( + static::jsonEncode($header), + static::jsonEncode($payload) + ); + $segments[] = Utils::UrlSafeBase64Encode(static::sign(\implode('.', $segments), $key, $alg)); + return \implode('.', $segments); + } + + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign + * @param string|resource $key The secret key + * @param string $alg The signing algorithm. + * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * + * @return string An encrypted message + */ + public static function sign(string $msg, $key, string $alg = 'HS256', string $passphrase = '') : string + { + switch ($alg) + { + case 'RS256': + case 'RS384': + case 'RS512': + $free_key = !\is_resource($key); + if ($free_key) { + $key = \openssl_pkey_get_private($key, $passphrase); + if (!$key) { + throw new \InvalidArgumentException('Invalid key, reason: ' . \openssl_error_string()); + } + } + try { + $details = \openssl_pkey_get_details($key); + if (!isset($details['key']) || OPENSSL_KEYTYPE_RSA !== $details['type']) { + throw new \InvalidArgumentException('Key is not compatible with RSA signatures'); + } + $signature = ''; + if (!\openssl_sign($msg, $signature, $key, 'SHA'.\substr($alg,2))) { + throw new \DomainException('OpenSSL unable to sign data: ' . \openssl_error_string()); + } + } finally { + if ($free_key) { + \openssl_pkey_free($key); + } + } + return $signature; + + case 'HS256': + case 'HS512': + case 'HS384': + $algo = 'SHA' . \substr($alg, 2); + if (\in_array($algo, \hash_algos())) { + return \hash_hmac($algo, $msg, $key, true); + } + + default: + throw new \InvalidArgumentException("Algorithm '{$alg}' not supported"); + } + } + + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key + * @param string $alg The algorithm + * + * @return bool + */ + private static function verify(string $msg, string $signature, $key, string $alg) : bool + { + switch ($alg) + { + case 'RS256': + case 'RS384': + case 'RS512': + $free_key = !\is_resource($key); + if ($free_key) { + $key = \openssl_pkey_get_public($key); + if (!$key) { + throw new \InvalidArgumentException('Invalid key, reason: ' . openssl_error_string()); + } + } + try { + $details = \openssl_pkey_get_details($key); + if (!isset($details['key']) || OPENSSL_KEYTYPE_RSA !== $details['type']) { + throw new \InvalidArgumentException('Key is not compatible with RSA signatures'); + } + $success = \openssl_verify($msg, $signature, $key, 'SHA'.\substr($alg,2)); + if (-1 == $success) { + throw new \DomainException('OpenSSL unable to verify data: ' . \openssl_error_string()); + } + } finally { + if ($free_key) { + \openssl_pkey_free($key); + } + } + return $success; + + case 'HS256': + case 'HS512': + case 'HS384': + $algo = 'SHA' . \substr($alg, 2); + if (\in_array($algo, \hash_algos())) { + return \hash_equals($signature, \hash_hmac($algo, $msg, $key, true)); + } + + // Ecdsa +// case 'ES256': +// case 'ES384': +// case 'ES512': + default: + throw new \InvalidArgumentException("Algorithm '{$alg}' not supported"); + } + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/backup.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/backup.php new file mode 100644 index 000000000..280724f80 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/backup.php @@ -0,0 +1,80 @@ +getMainAccountFromToken(); + if ($oAccount) { + $keyId = $keyId ? "0x{$keyId}" : \sha1($key); + $dir = $oActions->StorageProvider()->GenerateFilePath( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::PGP, + true + ); + if (\str_contains($key, 'PGP PRIVATE KEY')) { + $hash = $oAccount->CryptKey(); + $key = \SnappyMail\Crypt::Encrypt($key, $hash); + $key[1] = \base64_encode($key[1]); + $key[2] = \base64_encode($key[2]); + $key[] = \hash_hmac('sha1', $key[2], $hash); + return !!\file_put_contents("{$dir}{$keyId}.key", \json_encode($key)); + } + if (\str_contains($key, 'PGP PUBLIC KEY')) { + return !!\file_put_contents("{$dir}{$keyId}_public.asc", $key); + } + } + return false; + } + + public static function getKeys() : array + { + $result = [ + 'public' => [], + 'private' => [] + ]; + $oActions = \RainLoop\Api::Actions(); + $oAccount = $oActions->getMainAccountFromToken(); + if ($oAccount) { + $keyId = $keyId ? "0x{$keyId}" : \sha1($key); + $dir = $oActions->StorageProvider()->GenerateFilePath( + $oAccount, + \RainLoop\Providers\Storage\Enumerations\StorageType::PGP, + true + ); + $hash = $oAccount->CryptKey(); + foreach (\glob("{$dir}*") as $file) { + if (\is_file($file)) { + if ('_public.asc' === \substr($file, -11)) { + $result['public'][] = [ + 'id' => \basename($file), + 'value' => \file_get_contents($file), + ]; + } else if ('.key' === \substr($file, -4)) { + $key = \json_decode(\file_get_contents($file)); + if (\is_array($key)) { + $mac = \array_pop($key); + if (!empty($key[2]) && \hash_hmac('sha1', $key[2], $hash) === $mac) { + $key[1] = \base64_decode($key[1]); + $key[2] = \base64_decode($key[2]); + $result['private'][] = [ + 'id' => \basename($file), + 'value' => \SnappyMail\Crypt::Decrypt($key, $hash), + ]; + } + } + } + } + } + } + return $result; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php index 7e8382c17..947e8f47f 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gnupg.php @@ -10,7 +10,7 @@ class GnuPG { private $homedir, - // Instance of gnupg pecl extension + // Instance of gnupg pecl extension https://www.php.net/gnupg $GnuPG, // Instance of \SnappyMail\PGP\GPG $GPG; @@ -383,10 +383,10 @@ class GnuPG : $this->GPG->verify($signed_text, $signature, $plaintext); if (!$result) { if ($this->GnuPG) { - \SnappyMail\Log::notice('gnupg_verify() failed: ' . $this->GnuPG->geterror()); - \SnappyMail\Log::info(\print_r($this->GnuPG->geterrorinfo(),1)); + \SnappyMail\Log::notice('GnuPG', 'gnupg_verify() failed: ' . $this->GnuPG->geterror()); + \SnappyMail\Log::info('GnuPG', \print_r($this->GnuPG->geterrorinfo(),1)); } else { - \SnappyMail\Log::notice('GPG->verify() failed'); + \SnappyMail\Log::notice('GPG', 'GPG->verify() failed'); } } return $result; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gpg.php b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gpg.php index 432ec9f84..eb02644f7 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gpg.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/pgp/gpg.php @@ -60,6 +60,7 @@ class GPG $passphrases = [], $proc_resource, + $_openPipes, // GpgProcPipes $armor = true, @@ -188,6 +189,20 @@ class GPG return true; } + /** + * TODO: parse result + * https://github.com/the-djmaze/snappymail/issues/89 + */ + protected function listDecryptKeys(/*string|resource*/ $input, /*string|resource*/ $output = null) + { + $this->setInput($input); + $fclose = $this->setOutput($output); + $_ENV['PINENTRY_USER_DATA'] = ''; + $result = $this->exec(['--list-packets']); + $fclose && \fclose($fclose); + return $output ? true : ($result ? $result['output'] : false); + } + protected function _decrypt(/*string|resource*/ $input, /*string|resource*/ $output = null) { $this->setInput($input); @@ -245,6 +260,7 @@ class GPG */ public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ { + // TODO: https://github.com/the-djmaze/snappymail/issues/89 return false; } @@ -253,6 +269,7 @@ class GPG */ public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ { + // TODO: https://github.com/the-djmaze/snappymail/issues/89 return false; } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/repository.php b/snappymail/v/0.0.0/app/libraries/snappymail/repository.php index d5de7d92b..c105364a0 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/repository.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/repository.php @@ -112,33 +112,30 @@ abstract class Repository { $aResult = array(); try { - $notDev = '0.0.0' !== APP_VERSION; foreach (static::getRepositoryDataByUrl($bReal) as $oItem) { - if ($oItem && isset($oItem->type, $oItem->id, $oItem->name, - $oItem->version, $oItem->release, $oItem->file, $oItem->description)) - { - if ((!empty($oItem->required) && $notDev && \version_compare(APP_VERSION, $oItem->required, '<')) - || (!empty($oItem->deprecated) && $notDev && \version_compare(APP_VERSION, $oItem->deprecated, '>=')) - || (isset($aResult[$oItem->id]) && \version_compare($aResult[$oItem->id]['version'], $oItem->version, '>')) - ) { - continue; - } - - if ('plugin' === $oItem->type) { - $aResult[$oItem->id] = array( - 'type' => $oItem->type, - 'id' => $oItem->id, - 'name' => $oItem->name, - 'installed' => '', - 'enabled' => true, - 'version' => $oItem->version, - 'file' => $oItem->file, - 'release' => $oItem->release, - 'desc' => $oItem->description, - 'canBeDeleted' => false, - 'canBeUpdated' => true - ); - } + if ($oItem + && isset($oItem->type, $oItem->id, $oItem->name, $oItem->version, $oItem->release, $oItem->file, $oItem->description) + && 'plugin' === $oItem->type + // is this entry newer then an already defined one + && (empty($aResult[$oItem->id]) || \version_compare($aResult[$oItem->id]['version'], $oItem->version, '<')) + // does this entry require same or older app version + && (SNAPPYMAIL_DEV || empty($oItem->required) || \version_compare(APP_VERSION, $oItem->required, '>=')) + // is this entry not deprecated for current app version? + && (SNAPPYMAIL_DEV || empty($oItem->deprecated) || \version_compare(APP_VERSION, $oItem->deprecated, '<')) + ) { + $aResult[$oItem->id] = array( + 'type' => $oItem->type, + 'id' => $oItem->id, + 'name' => $oItem->name, + 'installed' => '', + 'enabled' => true, + 'version' => $oItem->version, + 'file' => $oItem->file, + 'release' => $oItem->release, + 'desc' => $oItem->description, + 'canBeDeleted' => false, + 'canBeUpdated' => true + ); } } } catch (\Throwable $e) { @@ -183,6 +180,33 @@ abstract class Repository ); } + public static function enablePackage(string $sName, bool $bEnable = true) : bool + { + if (!\strlen($sName)) { + return false; + } + + $oConfig = \RainLoop\Api::Config(); + + $aEnabledPlugins = static::getEnabledPackagesNames(); + + $aNewEnabledPlugins = array(); + if ($bEnable) { + $aNewEnabledPlugins = $aEnabledPlugins; + $aNewEnabledPlugins[] = $sName; + } else { + foreach ($aEnabledPlugins as $sPlugin) { + if ($sName !== $sPlugin && \strlen($sPlugin)) { + $aNewEnabledPlugins[] = $sPlugin; + } + } + } + + $oConfig->Set('plugins', 'enabled_list', \trim(\implode(',', \array_unique($aNewEnabledPlugins)), ' ,')); + + return $oConfig->Save(); + } + public static function getPackagesList() : array { empty($_ENV['SNAPPYMAIL_INCLUDE_AS_API']) && \RainLoop\Api::Actions()->IsAdminLoggined(); @@ -231,6 +255,7 @@ abstract class Repository public static function deletePackage(string $sId) : bool { \RainLoop\Api::Actions()->IsAdminLoggined(); + static::enablePackage($sId, false); return static::deletePackageDir($sId); } @@ -266,18 +291,18 @@ abstract class Repository } if ($sTmp) { - if (\class_exists('PharData')) { - $oArchive = new \PharData($sTmp, 0, $sRealFile); - } else { -// throw new \Exception('PHP Phar is disabled, you must enable it'); - $oArchive = new \SnappyMail\TAR($sTmp); - } if (!static::deletePackageDir($sId)) { throw new \Exception('Cannot remove previous plugin folder: '.$sId); } if ('.phar' === \substr($sRealFile, -5)) { $bResult = \copy($sTmp, APP_PLUGINS_PATH . \basename($sRealFile)); } else { + if (\class_exists('PharData')) { + $oArchive = new \PharData($sTmp, 0, $sRealFile); + } else { +// throw new \Exception('PHP Phar is disabled, you must enable it'); + $oArchive = new \SnappyMail\TAR($sTmp); + } $bResult = $oArchive->extractTo(\rtrim(APP_PLUGINS_PATH, '\\/')); } if (!$bResult) { diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/sasl.php b/snappymail/v/0.0.0/app/libraries/snappymail/sasl.php index d5c9bfb30..e022278af 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/sasl.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/sasl.php @@ -4,7 +4,7 @@ namespace SnappyMail; abstract class SASL { - public bool $base64 = false; + public bool $base64 = true; abstract public function authenticate(string $authcid, string $passphrase, ?string $authzid = null) : string; @@ -22,7 +22,7 @@ abstract class SASL { if (\preg_match('/^([A-Z2]+)(?:-(.+))?$/Di', $type, $m)) { $class = __CLASS__ . "\\{$m[1]}"; - if (\class_exists($class)) { + if (\class_exists($class) && $class::isSupported($m[2] ?? '')) { return new $class($m[2] ?? ''); } } @@ -32,9 +32,6 @@ abstract class SASL public static function isSupported(string $type) : bool { if (\preg_match('/^([A-Z2]+)(?:-(.+))?$/Di', $type, $m)) { - if ('XOAUTH2' === $m[1] || 'OAUTHBEARER' === $m[1]) { - $m[1] = 'OAUTH'; - } $class = __CLASS__ . "\\{$m[1]}"; return \class_exists($class) && $class::isSupported($m[2] ?? ''); } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/sasl/login.php b/snappymail/v/0.0.0/app/libraries/snappymail/sasl/login.php index 4a84b4835..52608a49f 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/sasl/login.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/sasl/login.php @@ -9,8 +9,10 @@ class Login extends \SnappyMail\SASL public function authenticate(string $username, string $passphrase, ?string $challenge = null) : string { - if ($challenge && 'Username:' !== $this->decode($challenge)) { - throw new \Exception("Invalid response: {$challenge}"); + // $challenge should be 'VXNlcm5hbWU6', but broken on some systems + // See https://github.com/the-djmaze/snappymail/issues/693 + if ($challenge && !\str_starts_with($this->decode($challenge), 'Username:')) { + throw new \Exception("Invalid response: {$this->decode($challenge)}"); } $this->passphrase = $passphrase; return $this->encode($username); @@ -18,6 +20,7 @@ class Login extends \SnappyMail\SASL public function challenge(string $challenge) : ?string { + // $challenge should be 'UGFzc3dvcmQ6' if ($challenge && 'Password:' !== $this->decode($challenge)) { throw new \Exception("invalid response: {$challenge}"); } diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/sasl/oauthbearer.php b/snappymail/v/0.0.0/app/libraries/snappymail/sasl/oauthbearer.php new file mode 100644 index 000000000..66bfb84a2 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/sasl/oauthbearer.php @@ -0,0 +1,21 @@ +encode("n,a={$username},\x01auth=Bearer {$passphrase}\x01\x01"); + } + + public static function isSupported(string $param) : bool + { + return true; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/sasl/oauth.php b/snappymail/v/0.0.0/app/libraries/snappymail/sasl/xoauth2.php similarity index 91% rename from snappymail/v/0.0.0/app/libraries/snappymail/sasl/oauth.php rename to snappymail/v/0.0.0/app/libraries/snappymail/sasl/xoauth2.php index cd73fb056..bf1563013 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/sasl/oauth.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/sasl/xoauth2.php @@ -7,7 +7,7 @@ namespace SnappyMail\SASL; -class OAuth extends \SnappyMail\SASL +class XOAuth2 extends \SnappyMail\SASL { public function authenticate(string $username, string $passphrase, ?string $authzid = null) : string { diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php b/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php index 0cb9e735f..163d4a2bc 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php @@ -77,7 +77,7 @@ class ZIP while ($i-- && \ob_end_clean()); \ob_get_level() || \header('Content-Encoding: '); } - \header('Cache-Control: no-store, no-cache, must-revalidate'); + \header('Cache-Control: no-store'); \header('Pragma: no-cache'); \header('Content-Transfer-Encoding: binary'); $name = "{$name}.zip"; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/tar.php b/snappymail/v/0.0.0/app/libraries/snappymail/tar.php index 84d466e8a..8ede5737d 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/tar.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/tar.php @@ -23,17 +23,15 @@ class TAR */ public function extractTo(string $directory, $files = null, bool $overwrite = false) : bool { - if ($files) { - \trigger_error('$files parameter not yet supported'); - return false; - } - $fp = \gzopen($this->filename, 'rb'); if (!$fp) { return false; } - \clearstatcache(false); + if ($files) { + $files = '/^(' . \implode('|', \array_map('preg_quote', \is_array($files) ? $files : [$files])) . ')/u'; + } + \clearstatcache(true); while (!\gzeof($fp)) { $data = \gzread($fp, 512); @@ -65,7 +63,13 @@ class TAR if (\preg_match('#(^|/)PaxHeader/#', $header['filename'])) { } else if (\substr($header['filename'], -1) !== '/') { $filename = ($header['path'] ? $header['path'] . '/' : '') . $header['filename']; + if ($files && !\preg_match($files, $filename)) { + continue; + } $filename = $directory . '/' . $filename; + if (\is_file($filename) && !$overwrite) { + continue; + } $dir = \dirname($filename); if (!\is_dir($dir) && !\mkdir($dir, 0777, true)) { return false; diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php b/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php index dc86c76c2..e1a131388 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php @@ -15,8 +15,8 @@ abstract class Upgrade $aEmail = \explode('@', \basename($sDomainDir)); $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); $sNewDir = $sDataPath - .'/'.\RainLoop\Utils::fixName($sDomain ?: 'unknown.tld') - .'/'.\RainLoop\Utils::fixName(\implode('@', $aEmail) ?: '.unknown'); + .'/'.\MailSo\Base\Utils::SecureFileName($sDomain ?: 'unknown.tld') + .'/'.\MailSo\Base\Utils::SecureFileName(\implode('@', $aEmail) ?: '.unknown'); if (\is_dir($sNewDir) || \mkdir($sNewDir, 0700, true)) { foreach (\glob("{$sDomainDir}/*") as $sItem) { $sName = \basename($sItem); @@ -130,13 +130,14 @@ abstract class Upgrade if (!$aData) { $aData = static::DecodeKeyValues($sData); if ($aData) { - $oActions->setContactsSyncData($oAccount, $aData); - return array( - 'Enable' => isset($aData['Enable']) ? !!$aData['Enable'] : false, + $aData = array( + 'Mode' => empty($aData['Enable']) ? 0 : 1, 'Url' => isset($aData['Url']) ? \trim($aData['Url']) : '', 'User' => isset($aData['User']) ? \trim($aData['User']) : '', 'Password' => isset($aData['Password']) ? $aData['Password'] : '' ); + $oActions->setContactsSyncData($oAccount, $aData); + return $aData; } } } @@ -197,16 +198,22 @@ abstract class Upgrade throw new \Exception('Failed to download latest SnappyMail'); } $target = \rtrim(APP_INDEX_ROOT_PATH, '\\/'); - $oArchive = new \PharData($sTmp, 0, null, \Phar::GZ); + if (\class_exists('PharData')) { + $oArchive = new \PharData($sTmp, 0, null, \Phar::GZ); + } else { + $oArchive = new \SnappyMail\TAR($sTmp); + } \error_log('Extract to ' . $target); // $bResult = $oArchive->extractTo($target, null, true); $bResult = $oArchive->extractTo($target, 'snappymail/') && $oArchive->extractTo($target, 'index.php', true); - if ($bResult) { - \error_log('Update success'); - } else { + if (!$bResult) { throw new \Exception('Extract core files failed'); } + + static::fixPermissions(); + + \error_log('Update success'); // opcache_reset is a terrible solution // \is_callable('opcache_reset') && \opcache_reset(); \is_callable('opcache_invalidate') && \opcache_invalidate($target.'/index.php', true); @@ -216,4 +223,30 @@ abstract class Upgrade } return $bResult; } + + // Prevents Apache access error due to directories being 0700 + public static function fixPermissions($mode = 0755) : void + { + \clearstatcache(true); + \umask(0022); + $target = \rtrim(APP_INDEX_ROOT_PATH, '\\/'); + // Prevent Apache access error due to directories being 0700 + foreach (\glob("{$target}/snappymail/v/*", \GLOB_ONLYDIR) as $dir) { + \chmod($dir, 0755); + foreach (['static','themes'] as $folder) { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator("{$dir}/{$folder}", \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $item) { + if ($item->isDir()) { + \chmod($item, 0755); + } else if ($item->isFile()) { + \chmod($item, 0644); + } + } + } + } + } + } diff --git a/snappymail/v/0.0.0/app/localization/ar-SA/user.json b/snappymail/v/0.0.0/app/localization/ar-SA/user.json index 3c6dc5ed8..1802c2abf 100644 --- a/snappymail/v/0.0.0/app/localization/ar-SA/user.json +++ b/snappymail/v/0.0.0/app/localization/ar-SA/user.json @@ -1,4 +1,5 @@ { + "LANG_DIR": "rtl", "GLOBAL": { "TO_ARCHIVE": "الأرشيف", "ARE_YOU_SURE": "هل أنت متأكد؟", @@ -19,21 +20,27 @@ "MOVE_TO": "..انقل الى", "NAME": "البريد الوارد", "NONE": "None", + "TO_SPAM": "Move message(s) to Spam", "NOT_SPAM": "ليست رسائل غير مرغوب فيها", "USERNAME": "اسم المستخدم", "PASSWORD": "كلمة السر", + "REMEMBER": "Remember", "REPLY_TO": "الرد-الى", "SAVE": "حفظ", "SAVE_CHANGES": "Save changes?", "SEARCH": "لم يتم ايجاد جهة الإتصال", - "SPAM": "الإبلاغ عن الرسائل غير المرغوب فيها", + "SPAM": "رسائل إلكترونية مزعجة", "SUBJECT": "الموضوع", "TEST": "تجريب", "TO": "الى", "SERVER_MESSAGE": "Server message", "YES": "نعم", "NO": "لا", - "INACTIVE": "Inactive" + "INACTIVE": "Inactive", + "DEFAULT": "الافتراضي", + "HELP": "المساعدة", + "TEXT": "النص", + "ATTACHMENTS": "الملحقات" }, "LOGIN": { "LABEL_LOGIN": "تسيجيل الدخول", @@ -41,13 +48,7 @@ "BUTTON_SIGN_IN": "الدخول" }, "TOP_TOOLBAR": { - "BUTTON_ADD_ACCOUNT": "ضفّ حساب", - "BUTTON_SETTINGS": "الإعدادات", - "BUTTON_HELP": "المساعدة" - }, - "MOBILE": { - "BUTTON_MOBILE_VERSION": "Mobile version", - "BUTTON_DESKTOP_VERSION": "Desktop version" + "BUTTON_ADD_ACCOUNT": "ضفّ حساب" }, "SEARCH": { "TITLE_ADV": "البحث المطور", @@ -98,6 +99,7 @@ "MENU_SELECT_SEEN": "مقروء", "MENU_SELECT_FLAGGED": "محدد كـ مهم", "MENU_SELECT_UNFLAGGED": "إزالة الأهمية", + "DOWNLOAD_ALL_ATTACHMENTS": "Download all attachments", "EMPTY_LIST": "إفراغ القائمة", "EMPTY_SEARCH_LIST": "ليست هناك أية رسائل تطابق بحثك.", "SEARCH_RESULT_FOR": "نتيجة البحث لـ \"%SEARCH%\"", @@ -108,6 +110,7 @@ "NEW_MESSAGE_NOTIFICATION": "يوجد لديك %COUNT% رسالة جديدة!", "QUOTA_SIZE": "Using %SIZE% (%PROC%%)<\/strong> of your %LIMIT%<\/strong>", "SORT": "Sort", + "NO_SORT": "Mail server does not support sorting", "SORT_DATE_ASC": "Date ascending", "SORT_DATE_DESC": "Date descending", "SORT_SIZE_ASC": "Size ascending", @@ -224,11 +227,16 @@ "NO_ATTACHMENTS_HERE_DESC": "لايوجد مرفقات", "ATTACHMENTS_ERROR_DESC": "تحذير! لم يتم إرفاق جميع المرفقات", "ATTACHMENTS_UPLOAD_ERROR_DESC": "لم تنتهي عملية الإرفاق بعد", + "ALLOW_SPELLCHECK": "Enable browser spelling checker", + "ALLOW_DRAFT_AUTOSAVE": "Automatically save draft", "BUTTON_REQUEST_READ_RECEIPT": "اطلب اعلاماً بالقراءة", "BUTTON_MARK_AS_IMPORTANT": "تحديده كـ هام", - "BUTTON_REQUEST_DSN": "اطلب اعلاماً بايصال البريد" + "BUTTON_REQUEST_DSN": "اطلب اعلاماً بايصال البريد", + "BUTTON_REQUIRE_TLS": "Require secure delivery", + "ERROR_EMPTY_BODY": "Message body is empty" }, "POPUPS_ASK": { + "EXIT_ARE_YOU_SURE": "Are you sure you want to exit?", "DESC_WANT_CLOSE_THIS_WINDOW": "هل أنت متأكد من رغبتك باغلاق هذه النافذة؟", "DESC_WANT_DELETE_MESSAGES": "هل أنت متأكد من رغبتك بحذف هذه الرسائل؟" }, @@ -312,7 +320,7 @@ "MARK_AS_READ_LABEL": "تعليمها بـ مقروءة", "REPLY_INTERVAL_LABEL": "فترة الرد (بلأيام)", "KEEP_LABEL": "الإحتفاظ بها", - "STOP_LABEL": "لاتتوقف عن المعالجة بالشروط", + "STOP_LABEL": "Stop further processing of rules", "VACATION_SUBJECT_LABEL": "الموضوع (خياري)", "VACATION_MESSAGE_LABEL": "الرسالة", "VACATION_RECIPIENTS_LABEL": "Recipients (comma separated)", @@ -351,21 +359,43 @@ "ERROR_UNKNOWN": "حدث خطأ نتيجة إضافة ملف غير معروف النوع" }, "EDITOR": { - "TEXT_SWITCHER_PLAIN_TEXT": "HTML <-> TEXT", - "TEXT_SWITCHER_SOURCE": "HTML <-> SOURCE" + "PLAIN": "HTML <-> TEXT", + "SOURCE": "HTML <-> SOURCE", + "FONTFAMILY": "Font family", + "FONTSIZE": "Font size", + "DIR": "Text direction", + "DIR_LTR": "Left to right", + "DIR_RTL": "Right to left", + "DIR_AUTO": "Automatic", + "TEXTCOLOR": "Text color", + "BACKGROUNDCOLOR": "Background color", + "BOLD": "Bold", + "ITALIC": "Italic", + "UNDERLINE": "Underline", + "STRIKE": "Strikethrough", + "SUB": "Subscript", + "SUP": "Superscript", + "OL": "Ordered list", + "UL": "Unordered list", + "QUOTE": "Blockquote", + "INDENTDECREASE": "Decrease indent", + "INDENTINCREASE": "Increase indent", + "LINK": "Link", + "IMAGEURL": "Image URL", + "IMAGEUPLOAD": "Image select", + "UNDO": "Undo", + "REDO": "Redo" }, "SETTINGS_LABELS": { - "LABEL_PERSONAL_NAME": "الشخصي", - "LABEL_GENERAL_NAME": "العام", - "LABEL_CONTACTS_NAME": "جهات الإتصال", - "LABEL_FOLDERS_NAME": "المجلدات", - "LABEL_ACCOUNTS_NAME": "الحسابات", - "LABEL_FILTERS_NAME": "عوامل التصفية", - "LABEL_SECURITY_NAME": "الأمان", - "LABEL_THEMES_NAME": "المظاهر العامة" + "GENERAL": "العام", + "CONTACTS": "جهات الإتصال", + "FOLDERS": "المجلدات", + "ACCOUNTS": "الحسابات", + "FILTERS": "عوامل التصفية", + "SECURITY": "الأمان", + "THEMES": "المظاهر العامة" }, "SETTINGS_FILTERS": { - "LEGEND_FILTERS": "عوامل التصفية", "SIMPLE": "Simple", "ADVANCED": "Advanced", "BUTTON_ADD_SCRIPT": "Add a Script", @@ -378,18 +408,18 @@ "SETTINGS_SECURITY": { "LEGEND_SECURITY": "الأمان", "LABEL_AUTOLOGOUT": "تسجيل الخروج التلقائي", - "AUTOLOGIN_NEVER_OPTION_NAME": "أبداَ", - "AUTOLOGIN_MINUTES_OPTION_NAME": "%MINUTES% دقيقة", - "AUTOLOGIN_HOURS_OPTION_NAME": "%HOURS% hour(s)" + "AUTOLOGIN_NEVER_OPTION_NAME": "أبداَ" }, "SETTINGS_GENERAL": { "LEGEND_GENERAL": "عام", "LANGUAGE": "اللغة", + "HOUR_FORMAT": "Hour format", "IDENTITY": "الهوية", "LAYOUT": "التصميم", "LAYOUT_NO_SPLIT": "بدون إنقسام", "LAYOUT_VERTICAL_SPLIT": "إنقسام عمودي", "LAYOUT_HORIZONTAL_SPLIT": "إنقسام أفقي", + "SHOW_NEXT_MESSAGE": "Show next message when (re)move current message", "EDITOR": "المحرر النصي الأساسي", "EDITOR_HTML": "Html", "EDITOR_PLAIN": "عادي", @@ -399,19 +429,30 @@ "USE_CHECKBOXES_IN_LIST": "عرض مربعات التحديد في القائمة", "USE_THREADS": "إستخدام المواضيع", "REPLY_SAME_FOLDER": "ضع الردود في مجلد الرسالة التي يتم الرد عليها", - "SHOW_IMAGES": "قم دائماَ بعرض الصور القادمة في الرسالة", "PREFER_HTML": "Prefer HTML to plain text", "PREFER_HTML_INFO": "Messages sometimes come in both formats. This option controls whether you want the HTML part or the plain text part to be displayed.", "REMOVE_COLORS": "Remove background and text colors from message body", + "ALLOW_STYLES": "Allow - - -

{{ErrorHeader}}

-
-
-

{{ErrorDesc}}

- Google Chrome - Microsoft Edge - Mozilla Firefox -
- - diff --git a/snappymail/v/0.0.0/app/templates/Index.html b/snappymail/v/0.0.0/app/templates/Index.html index 72dd73783..bd5c60d07 100644 --- a/snappymail/v/0.0.0/app/templates/Index.html +++ b/snappymail/v/0.0.0/app/templates/Index.html @@ -1,10 +1,6 @@ - - @@ -15,17 +11,26 @@ - + + {{BaseAppFaviconPngLinkTag}} {{BaseAppFaviconTouchLinkTag}} -
{{LoadingDescriptionEsc}}
+ +
+ + + -
-
diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminPane.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminPane.html index b261adad7..d9f410e46 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminPane.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminPane.html @@ -1,6 +1,5 @@
- +

SnappyMail -

-
diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html index 378efa1c0..0dba1b4dd 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsAbout.html @@ -4,7 +4,7 @@
- 2022 © + 2023 ©
https://snappymail.eu/
@@ -34,7 +34,8 @@
- + +
@@ -50,6 +51,23 @@
+

System

+ + + + + + + + + + + + + + +
Average load past minute
Average load past 5 minutes
Average load past 15 minutes
+

PHP optional extensions

diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsConfig.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsConfig.html index 019dc5e3d..cf7f6024b 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsConfig.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsConfig.html @@ -1,5 +1,4 @@
application.ini
-

WARNING: experimental editor!

Extension
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 8fe6c3833..d15b120d4 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 @@ -18,6 +18,14 @@ }"> +
+ +
+ +   + +
+
@@ -56,6 +64,29 @@ data-bind="value: contactsPdoPassword, saveTrigger: contactsPdoPasswordTrigger"> + +
+
+ +
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+ - -   - -
-
+

@@ -31,4 +27,16 @@ + + +
+ +    + +
+ +
+ + +
diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPackages.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPackages.html index 41e06a9c7..b9bc9e544 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPackages.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPackages.html @@ -1,5 +1,5 @@
- +
@@ -15,7 +15,7 @@

@@ -28,7 +28,7 @@
-
+
diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPluginProperty.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPluginProperty.html index 0e952f2af..130d710dc 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPluginProperty.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsPluginProperty.html @@ -1,33 +1,36 @@ - - + + - + - + - + - - + + - +
- + - - + + + + + diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html index 32f3e8dc4..2e5e27c57 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/AdminSettingsSecurity.html @@ -13,8 +13,7 @@ name: 'Checkbox', params: { value: capaOpenPGP, - label: 'TAB_SECURITY/LABEL_ALLOW_OPEN_PGP', - inline: true + label: 'TAB_SECURITY/LABEL_ALLOW_OPEN_PGP' } }">
@@ -57,28 +56,3 @@ data-icon="🔑" data-i18n="GLOBAL/SAVE"> -
-
-
-
-
- () -
-
-
-
-
diff --git a/snappymail/v/0.0.0/app/templates/Views/Admin/PopupsDomain.html b/snappymail/v/0.0.0/app/templates/Views/Admin/PopupsDomain.html index 991093c9d..bb015038c 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Admin/PopupsDomain.html +++ b/snappymail/v/0.0.0/app/templates/Views/Admin/PopupsDomain.html @@ -8,7 +8,7 @@ ()
-
@@ -18,82 +18,169 @@
- - + +
+
-
+

-
-
+

- - - + +
-
+

- + +
+
+ +
+

-
(user@domain.com → user) + +

SSL/TLS

+
+
+ +

+
+
+
+
+
+
+ +

+
+ + +
+
- - + +