diff --git a/.docker/release/Dockerfile b/.docker/release/Dockerfile
index d6675462e..0e6473191 100644
--- a/.docker/release/Dockerfile
+++ b/.docker/release/Dockerfile
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
-FROM alpine:3.18.5 AS builder
-RUN apk add --no-cache php82 php82-json php-phar php-zip
+FROM alpine:3.24.3 AS builder
+RUN apk add --no-cache php85 php85-json php-phar php-zip
RUN apk add --no-cache npm
RUN npm install -g gulp yarn
WORKDIR /source
@@ -23,7 +23,7 @@ RUN set -eux; \
rm -v /snappymail/README.md /snappymail/_include.php
# Inspired by the original Rainloop dockerfile from youtous on GitLab
-FROM php:8.2-fpm-alpine AS final
+FROM php:8.5-fpm-alpine AS final
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
@@ -99,9 +99,6 @@ RUN set -eux; \
# mysql
RUN docker-php-ext-install pdo_mysql
-# opcache
-RUN docker-php-ext-install opcache
-
# postgres
RUN set -eux; \
apk add --no-cache postgresql-libs; \
@@ -122,6 +119,13 @@ RUN set -eux; \
apk del .build-dependencies; \
apk del .deps
+# sqlite
+RUN set -eux; \
+ apk add --no-cache sqlite-libs; \
+ apk add --no-cache --virtual .deps sqlite-dev; \
+ docker-php-ext-install pdo_sqlite; \
+ apk del .deps
+
# tidy
RUN set -eux; \
apk add --no-cache tidyhtml; \
diff --git a/.docker/release/files/entrypoint.sh b/.docker/release/files/entrypoint.sh
index 75e82d729..c0ed7df0c 100755
--- a/.docker/release/files/entrypoint.sh
+++ b/.docker/release/files/entrypoint.sh
@@ -9,6 +9,12 @@ UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
SECURE_COOKIES=${SECURE_COOKIES:-true}
+# Disable listening on ipv6 for ipv4-only environments, or nginx will fail to start
+if ! ip a | grep 'inet6 ' > /dev/null; then
+ echo "[INFO] ipv6 is disabled. Configuring nginx to not listen on ipv6"
+ sed -i '/listen \[::\]:/d' /etc/nginx/nginx.conf
+fi
+
# Set attachment size limit
sed -i "s//$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
sed -i "s//$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf
diff --git a/.docker/release/files/etc/nginx/nginx.conf b/.docker/release/files/etc/nginx/nginx.conf
index ed87cb201..aff3829cf 100644
--- a/.docker/release/files/etc/nginx/nginx.conf
+++ b/.docker/release/files/etc/nginx/nginx.conf
@@ -52,6 +52,8 @@ http {
server {
listen 8888;
+ listen [::]:8888;
+ #listen [::]:8888 ipv6only=off;
root /snappymail;
index index.php;
charset utf-8;
diff --git a/.eslintrc.js b/.eslintrc.js
index 5e2334f0a..e22cb986d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -3,7 +3,7 @@ module.exports = {
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
extends: ['eslint:recommended'],
parserOptions: {
- ecmaVersion: 6,
+ ecmaVersion: 11,
sourceType: 'module'
},
env: {
@@ -37,7 +37,11 @@ module.exports = {
// Mailvelope
'mailvelope': "readonly",
// Punycode
- 'IDN': "readonly"
+ 'IDN': "readonly",
+ // Turndown
+ 'TurndownService': "readonly",
+ // Marked
+ 'marked': "readonly"
},
// http://eslint.org/docs/rules/
rules: {
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d53d388ed..998692964 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -30,8 +30,9 @@ If applicable, add screenshots to help explain your problem.
- 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)
+**Debug/logging information**
+[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
+- [ ] I've placed them here (few lines) or as attachments (many lines)
**Additional context**
Add any other context about the problem here.
diff --git a/.gitignore b/.gitignore
index b4e8f72e4..5f49d65d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,8 @@
/vendors/knockout/spec
/vendors/openpgp-5
!/vendors/openpgp-5/dist
+/vendors/squire2
+!/vendors/squire2/dist
/vendors/vanillaqr.js/
/integrations/nextcloud/rainloop
/integrations/owncloud/rainloop
diff --git a/.gitmodules b/.gitmodules
index cfb771834..ccf1009a6 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,7 @@
[submodule "vendors/openpgp-5"]
path = vendors/openpgp-5
url = git@github.com:the-djmaze/openpgpjs.git
+[submodule "vendors/squire2"]
+ path = vendors/squire2
+ url = git@github.com:the-djmaze/Squire.git
+ branch = snappymail
diff --git a/.htaccess b/.htaccess
index faa8201c8..71041371a 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,7 +1,15 @@
+
+ AcceptPathInfo On
+
+
RewriteEngine On
# Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
+
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.+)$ index.php/$1 [L,QSA]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08c855b4c..d31a1b771 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,380 @@
+## 2.38.2 – 2024-10-09
+### Fixed
+- error '$index is not defined' in Settings > Accounts
+ [#1797](https://github.com/the-djmaze/snappymail/issues/1797)
+
+
+## 2.38.1 – 2024-10-08
+
+### Added
+- Admin - Extensions search filter
+- Options to unset current font family and size
+ [#1726](https://github.com/the-djmaze/snappymail/issues/1726)
+- Option to allow insecure cryptkey
+ [#1746](https://github.com/the-djmaze/snappymail/issues/1746)
+- Save vCard FN property
+ [#1761](https://github.com/the-djmaze/snappymail/issues/1761)
+- Docker Nginx should listen on IPv6 addresses in addition to IPv4
+ [#1770](https://github.com/the-djmaze/snappymail/issues/1770)
+- Full-screen Message View on Double-Click
+ [#1787](https://github.com/the-djmaze/snappymail/issues/1787)
+
+### Changed
+- Use the custom Squire as submodule vendors/squire2
+- Disallow noembed and noframes HTML elements
+- Keep 1 space between HTML elements in compressed templates
+- Squire: sanitizeToDOMFragment now uses cleanHTML()
+- Squire: improved handling of BR elements
+ [#1389](https://github.com/the-djmaze/snappymail/issues/1389)
+- Nextcloud border-radius by @gnilebein
+ [#1790](https://github.com/the-djmaze/snappymail/pull/1790)
+- Compose window sentFolder handling for
+ [#1793](https://github.com/the-djmaze/snappymail/pull/1793)
+- Update Portuguese by @ner00
+- Update Polish by @tinola
+- Update French by @hguilbert
+- Update Portuguese (Brazil) by @mstolf
+
+### Fixed
+- Composer dialog scroll got broken in v2.28
+- Composer dialog "from" triangle button wrong position due to font changes
+- Admin - Config `search` should be ko.observable() not ko.observableArray()
+- Squire: paste images
+ [#1389](https://github.com/the-djmaze/snappymail/issues/1389)
+- Domain whitelist failures on login
+ [#1706](https://github.com/the-djmaze/snappymail/issues/1706)
+- Sieve parse errors
+- Sieve support `index` was not optional for
+ [#1709](https://github.com/the-djmaze/snappymail/issues/1709)
+- Pagination problem for large mailbox after 2.36.1
+ [#1716](https://github.com/the-djmaze/snappymail/issues/1716)
+- Undefined constant "LOG_ERROR"
+ [#1754](https://github.com/the-djmaze/snappymail/issues/1754)
+- Search Filter Capital "B" not working
+ [#1780](https://github.com/the-djmaze/snappymail/issues/1780)
+- PHP 8.4: Implicitly nullable parameter declarations deprecated
+
+
+## 2.38.0 – 2024-09-16
+
+### Added
+- Remove `tel:` links when converting HTML to plain
+ [#1724](https://github.com/the-djmaze/snappymail/issues/1724)
+- Convert HTML to Markdown instead of plain, by using [Turndown](https://github.com/mixmark-io/turndown)
+ [#1604](https://github.com/the-djmaze/snappymail/issues/1604)
+- Check HTMLInputElement.validity() for
+ [#1733](https://github.com/the-djmaze/snappymail/issues/1733)
+
+### Changed
+- Use a modified [Squire 2.3.2](https://github.com/the-djmaze/Squire/commits/snappymail/)
+- cleanHtml use allowedTags instead of disallowedTags and improved CSS handling
+- Update Portuguese by @ner00
+
+### Fixed
+- mXSS exploit found by SonarSource
+ [CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
+- Call to a member function Email() on null
+ [#1706](https://github.com/the-djmaze/snappymail/issues/1706)
+- IMAP capabilities via IMAP Proxy
+ [#1725](https://github.com/the-djmaze/snappymail/issues/1725)
+- Messages on page setting is not validated against 999 max
+ [#1733](https://github.com/the-djmaze/snappymail/issues/1733)
+
+
+## 2.37.3 – 2024-08-27
+
+### Added
+- Mark images with width=1 as tracking pixel
+- Show warning in Admin -> About when PHP runs in 32bit
+- Edit ACL rules
+ [#157](https://github.com/the-djmaze/snappymail/issues/157)
+- Show GnuPG version for
+ [#1560](https://github.com/the-djmaze/snappymail/issues/1560)
+- Make sure only scalar values are allowed in $_ENV for
+ [#1560](https://github.com/the-djmaze/snappymail/issues/1560)
+- Change minimum new mail check interval
+ [#1678](https://github.com/the-djmaze/snappymail/issues/1678)
+- Sieve editor does not support "index" extension
+ [#1709](https://github.com/the-djmaze/snappymail/issues/1709)
+
+### Changed
+- Improved domain autoconfig interaction
+- MS autodiscover priorities DNS over subdomain
+- Simplify sieve scripts list
+ [#1675](https://github.com/the-djmaze/snappymail/issues/1675)
+- Handling of (token) errors due to
+ [#1706](https://github.com/the-djmaze/snappymail/issues/1706)
+- Sabre/Xml to v4.0.5
+- Update Chinese by @Artoria2e5
+- Update French by @hguilbert
+
+### Fixed
+- Thread sorting visible after disabling the imap capability
+ [#1574](https://github.com/the-djmaze/snappymail/issues/1574)
+- Creating new message impossible as long as a draft exists?
+ [#1710](https://github.com/the-djmaze/snappymail/issues/1710)
+- InvalidToken error at login
+ [#1706](https://github.com/the-djmaze/snappymail/issues/1706)
+
+### Nextcloud
+- Force Nextcloud personal language by default
+ [#1428](https://github.com/the-djmaze/snappymail/issues/1428)
+
+
+## 2.37.2 – 2024-08-13
+
+### Added
+- Validate Fetch JSON response
+
+### Fixed
+- PATH_INFO bug due to Office365 OAuth login
+- Prevent logout loop on error
+
+### Nextcloud
+- Failed loading due to Office365 OAuth2 attempt
+ [#1703](https://github.com/the-djmaze/snappymail/issues/1703)
+
+
+## 2.37.1 – 2024-08-12
+
+### Fixed
+- Gulp v5 broke the fonts
+
+### Nextcloud
+- Support v30
+
+
+## 2.37.0 – 2024-08-12
+
+### Added
+- JavaScript event `rl-vm-visible`
+- Detailed error message on account switch failure for
+ [#1594](https://github.com/the-djmaze/snappymail/issues/1594)
+- Workarounds for Microsoft OAuth2 (currently requires Apache AcceptPathInfo)
+ [#1645](https://github.com/the-djmaze/snappymail/issues/1645)
+- Support "mark for deletion"
+ [#1657](https://github.com/the-djmaze/snappymail/issues/1657) by @smsoft-ru
+- Invoke "Update Identity" pop up right after login (when not initialized)
+ [#1689](https://github.com/the-djmaze/snappymail/issues/1689)
+- Keyboard shortcut for "Swap default (background) color"
+ [#1690](https://github.com/the-djmaze/snappymail/issues/1690)
+
+### Changed
+- Updated gulp to v5
+- Replaced vulnerable gulp-header with gulp-append-prepend
+- Removed abandoned vulnerable rollup-plugin-html
+- Align save button in admin security settings
+- Made registerProtocolHandler('mailto') optional by activating at Settings -> General
+- Improved InvalidToken handling for
+ [#1653](https://github.com/the-djmaze/snappymail/issues/1653)
+- Cleanup localizations
+- Update French by @hguilbert
+- Update German by @tkasch
+- Update Polish by @tinola
+- Update Portuguese by @ner00
+
+### Fixed
+- prevent multiple afterShow() and afterHide() due to `transitionend` on multiple CSS properties
+- Attempt to read property "smimeSigned" on null
+- Refreshing mail list doesn't update current message
+ [#1654](https://github.com/the-djmaze/snappymail/issues/1654)
+- Deletion of CACHE folder causing error
+ [#1660](https://github.com/the-djmaze/snappymail/issues/1660)
+- Multiple line breaks are not displayed
+ [#1666](https://github.com/the-djmaze/snappymail/issues/1666)
+- RainLoop\Exceptions\ClientException::__construct(): Argument #2 ($oPrevious) must be of type ?Throwable, string given
+ [#1686](https://github.com/the-djmaze/snappymail/issues/1686)
+- SpamAssassin Division by zero
+ [#1694](https://github.com/the-djmaze/snappymail/issues/1694)
+- Failed to parse RFC 2822 date '6 Jul 2024 16:42:09 +0200'
+ [#1694](https://github.com/the-djmaze/snappymail/issues/1694)
+- Fix capabilities when THREAD is disabled
+ [#1698](https://github.com/the-djmaze/snappymail/pull/1698) by @akhil1508
+
+### Nextcloud
+- Failed loading due to incorrect `app_path`
+- Bugfix language detection
+- Allow multi-account in nc with oauth login
+ [#1699](https://github.com/the-djmaze/snappymail/pull/1699) by @akhil1508
+
+
+## 2.36.4 – 2024-06-25
+
+### Added
+- Customize private key passphrase expiration interval
+ [#1545](https://github.com/the-djmaze/snappymail/discussions/1545)
+- AdvancedSearch support for filtering mails before a given date
+ [#1606](https://github.com/the-djmaze/snappymail/pull/1606) by @codiflow
+- Control valid spam and virus headers
+ [#1607](https://github.com/the-djmaze/snappymail/issues/1607)
+- Remember S/MIME private Key without function
+ [#1611](https://github.com/the-djmaze/snappymail/issues/1611)
+- Resize compose dialog
+- Magnetic theme
+ [#1637](https://github.com/the-djmaze/snappymail/pull/1637) by @TheCuteFoxxy
+
+### Changed
+- Improved signing messages by allowing to choose between the options
+- Improved language detection code
+- More detailed Decrypt errors
+- Update French by @hguilbert
+- Update Polish by @tinola
+- Update Portuguese by @ner00
+- Update Spanish by @huloza
+
+### Fixed
+- Default language error
+- Undefined $sEmail in DoAdminDomainMatch
+- Handling Autocrypt header failed on `=`
+ [#1608](https://github.com/the-djmaze/snappymail/issues/1608)
+- Blank lines are inserted when editing draft
+ [#1609](https://github.com/the-djmaze/snappymail/issues/1609)
+- Workaround Cyrus MAILBOXID bug (disable OBJECTID capability by default due to impact)
+ [#1640](https://github.com/the-djmaze/snappymail/issues/1640)
+- Workaround HTML with multiple body elements or MIME with multiple text/html
+ [#1641](https://github.com/the-djmaze/snappymail/issues/1641)
+
+### Nextcloud
+- OIDC stay logged in
+ [#1620](https://github.com/the-djmaze/snappymail/pull/1620) by @avinash-0007
+
+
+## 2.36.3 – 2024-05-27
+
+### Changed
+- UserAuth prevent plugin errors (like the Nextcloud plugin did)
+
+### Fixed
+- Undefined variable $aTokenData
+ [#1567](https://github.com/the-djmaze/snappymail/issues/1567)
+
+
+## 2.36.2 – 2024-05-26
+
+### Added
+- "copy to" action in menu's for
+ [#1559](https://github.com/the-djmaze/snappymail/issues/1559)
+- Log signal info for
+ [#1569](https://github.com/the-djmaze/snappymail/issues/1569)
+- OpenPGP.js automatically import backup keys from server
+
+### Changed
+- Improved "remember me" cookie handling
+- Update Basque by @Thadah
+- Update Portuguese by @ner00
+
+### Fixed
+- "Account already exists"
+ [#1561](https://github.com/the-djmaze/snappymail/issues/1561)
+- Properly escape path separator in tar.php file list regex
+ [#1562](https://github.com/the-djmaze/snappymail/pull/1562) by @sevmonster
+- Prevent mkdir() error
+ [#1565](https://github.com/the-djmaze/snappymail/issues/1565)
+- SCRAM Exception when trying to log in to SMTP
+ [#1575](https://github.com/the-djmaze/snappymail/issues/1575)
+- Error when redirected back to instance after Gmail OAuth
+ [#1580](https://github.com/the-djmaze/snappymail/issues/1580)
+- Uncaught TypeError: hasPublicKeyForEmails(...).then is not a function
+ [#1589](https://github.com/the-djmaze/snappymail/issues/1589)
+- Undefined variable $sFilename
+- GPG/PGP exec() return false handling
+
+### Nextcloud
+- OIDC login active again
+ [#1572](https://github.com/the-djmaze/snappymail/pull/1572) by @avinash-0007
+
+
+## 2.36.1 – 2024-04-23
+
+### Added
+- Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX
+- Have I Been Pwned class to check breached passwords and email addresses
+- Handle RFC 5987 in Content-Disposition header
+- Ignore text/x-amp-html
+- Show SMTP error to user
+ [#1521](https://github.com/the-djmaze/snappymail/issues/1521)
+- OAuth2 for login using gmail (and others)
+
+### Changed
+- logMask all AUTHENTICATE requests
+- ErrorTip use white-space: pre
+- Simplify LoginProcess handling
+- ES2020 everywhere (require Safari 13.1)
+- Modified Squire to be more in line with v2.2.8
+- CSS set min-width for .attachmentParent and .flagParent to line them up
+- cPanel use extension login-cpanel instead of login-remote
+- Improved login credentials handling
+- Speedup Knockout a bit and removed `with($context)` scope
+- Update Belarusian by @spoooyders
+- Update Chinese by @mayswind
+- Update French by @hguilbert
+- Update Polish by @tinola
+- Update Portuguese by @ner00
+
+### Fixed
+- Content encoding and type detection in JavaScript could fail due to case-sensitivity.
+- Extensions set logger failed
+- GnuPG check open_basedir and if shell_exec is disabled
+ [#1385](https://github.com/the-djmaze/snappymail/issues/1385)
+ [#1496](https://github.com/the-djmaze/snappymail/issues/1496)
+ [#1555](https://github.com/the-djmaze/snappymail/issues/1555)
+- Hide pagination when search result has no messages
+- Prevent mbstring error before setup.php
+- Prevent MessagesPerPage Infinity
+ [#1540](https://github.com/the-djmaze/snappymail/issues/1540)
+- Reseal CryptKey failed
+ [#1543](https://github.com/the-djmaze/snappymail/issues/1543)
+
+### Nextcloud
+- Add an occ command to set up the login settings
+ [#1552](https://github.com/the-djmaze/snappymail/issues/1552)
+
+
+## 2.36.0 – 2024-03-18
+
+### Added
+- Allow setting the supported THREAD algorithm
+- Icon to system folders
+- Remove remembered password after 15 minutes of inactivity
+ [#1142](https://github.com/the-djmaze/snappymail/issues/1142)
+- Swap background and text color for unreadable text on dark background
+ [#1486](https://github.com/the-djmaze/snappymail/issues/1486)
+- Generate TOTP code at ?Admin -> Security
+ [#1501](https://github.com/the-djmaze/snappymail/issues/1501)
+- Button to change S/MIME private key passphrase
+ [#1505](https://github.com/the-djmaze/snappymail/issues/1505)
+- Belarusian
+ [#1512](https://github.com/the-djmaze/snappymail/pull/1512) by @spoooyders
+- Log some domain idn_to_ascii issues
+ [#1513](https://github.com/the-djmaze/snappymail/issues/1513)
+
+### Changed
+- On folder/mailbox rename, also rename all children instead of reloading all
+- Seal MainAccount CryptKey and on error ask old login passphrase to reseal key.
+- Moved cache drivers outside core into extensions
+- Sieve always allow fileinto INBOX
+ [#1510](https://github.com/the-djmaze/snappymail/issues/1510)
+- Moved application.ini `sieve_auth_plain_initial` to per domain config
+- Languages use rfc5646, by using the shortest ISO 639 code by default
+- Update French by @hguilbert
+- Update Portuguese by @ner00
+
+### Fixed
+- On folder/mailbox rename, the old fullName must be removed from cache
+- On folder/mailbox rename, the checkable option was not renamed
+- Sort accounts drag & drop
+- S/MIME encrypted and opaque signed not visible
+ [#1450](https://github.com/the-djmaze/snappymail/issues/1450)
+- Wrong last UID of thread
+ [#1507](https://github.com/the-djmaze/snappymail/issues/1507)
+- Creation of dynamic property SnappyMail\DAV\Client::$HTTP
+ [#1509](https://github.com/the-djmaze/snappymail/issues/1509)
+- "Download as ZIP" fails for messages
+ [#1514](https://github.com/the-djmaze/snappymail/issues/1514)
+- SMTP "Authentication failed" when IMAP uses `shortLogin` and SMTP not
+ [#1517](https://github.com/the-djmaze/snappymail/issues/1517)
+
+
## 2.35.4 – 2024-03-16
### Added
@@ -6,7 +383,6 @@
### Changed
- OpenPGP.js to v5.11.1
- punycode.js lowercase domain names
-- Nextcloud changed stored password handling
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
- Update Portuguese by @ner00
@@ -21,6 +397,11 @@
[#1500](https://github.com/the-djmaze/snappymail/issues/1500)
- VCard PHP Notice: Undefined index: ENCODING
+### Nextcloud
+- Changed stored password handling
+- Can't login from nextcloud with 2.35.3 bug Nextcloud
+ [#1490](https://github.com/the-djmaze/snappymail/issues/1490)
+
## 2.35.3 – 2024-03-12
@@ -68,8 +449,9 @@
- Crash on importing corrupt OpenPGP keys
- Crash on old browsers instead of showing error
- Ignore popups on logoutReload()
+ [#1467](https://github.com/the-djmaze/snappymail/issues/1467)
- Custom SASLMechanisms fail in IMAP when the connection is secure
- [#1484](https://github.com/the-djmaze/snappymail/pull/1484)
+ [#1484](https://github.com/the-djmaze/snappymail/pull/1484) by @botsarenthuman
## 2.35.2 – 2024-02-27
@@ -86,7 +468,9 @@
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues
- Error 352 when getting Filters
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
-- Nextcloud SetPassword(): Argument #1 must be of type SensitiveString, string given
+
+### Nextcloud
+- SetPassword(): Argument #1 must be of type SensitiveString, string given
[#1456](https://github.com/the-djmaze/snappymail/issues/1456)
@@ -267,8 +651,8 @@
- Run full GetUids() in background when message_list_limit is set
- MessageListThreadsMap as background task when message_list_limit is set
- Properly set CACHEDIR.TAG
-- Sending group email to all contact addresses by @rezaei92
- [#1286](https://github.com/the-djmaze/snappymail/pull/1286)
+- Sending group email to all contact addresses
+ [#1286](https://github.com/the-djmaze/snappymail/pull/1286) by @rezaei92
### Changed
- Default IMAP message_list_limit to 10000
@@ -276,8 +660,8 @@
- Improved Squire WYSIWYG
- Sort real attachments and inline attachments for
[#1360](https://github.com/the-djmaze/snappymail/issues/1360)
-- Nextcloud Theme fixes and improvements by @hampoelz
- [#1363](https://github.com/the-djmaze/snappymail/pull/1363)
+- Nextcloud Theme fixes and improvements
+ [#1363](https://github.com/the-djmaze/snappymail/pull/1363) by @hampoelz
- Improve display of attachments
[#1361](https://github.com/the-djmaze/snappymail/issues/1361)
- Rename messageVisibility to messageVisible
@@ -291,8 +675,8 @@
- Preview tooltip shows "null" when PREVIEW capability is disabled
### Nextcloud
-- Improved language handling by @avinash-0007
- [#1362](https://github.com/the-djmaze/snappymail/pull/1362)
+- Improved language handling
+ [#1362](https://github.com/the-djmaze/snappymail/pull/1362) by @avinash-0007
- FilterLanguage had wrong parameter order
- Use NextcloudV25+ theme by default
@@ -313,8 +697,8 @@
[#1344](https://github.com/the-djmaze/snappymail/issues/1344)
- NO Mailbox does not exist, or must be subscribed to")
[#1354](https://github.com/the-djmaze/snappymail/issues/1354)
-- Flag indicators are added to wrong message by @SergeyMosin
- [#1347](https://github.com/the-djmaze/snappymail/pull/1347)
+- Flag indicators are added to wrong message
+ [#1347](https://github.com/the-djmaze/snappymail/pull/1347) by @SergeyMosin
- Squire: issue when using the enter key in a reply window
[#1296](https://github.com/the-djmaze/snappymail/issues/1296)
- Squire: crash on cut/delete range
@@ -428,8 +812,8 @@
[#1308](https://github.com/the-djmaze/snappymail/issues/1308)
- With email open, "delete" doesn't delete #1274
[#1274](https://github.com/the-djmaze/snappymail/issues/1274)
-- Fix threading view in Thunderbird (others?) by @tkasch
- [#1304](https://github.com/the-djmaze/snappymail/pull/1304)
+- Fix threading view in Thunderbird (others?)
+ [#1304](https://github.com/the-djmaze/snappymail/pull/1304) by @tkasch
## 2.29.1 – 2023-10-02
@@ -443,18 +827,18 @@
### Added
- Modern UI / Nextcloud Theme
[#629](https://github.com/the-djmaze/snappymail/pull/629) by @hampoelz
-- "Add/Edit signature" label to PopupsIdentity.html by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
+- "Add/Edit signature" label to PopupsIdentity.html
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- use calendar icon in message list for messages with '.ics' or 'text/calendar' attachments by @SergeyMosin
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- Show unseen message count when the message list is threaded by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- in mobile mode hide folders(left) panel when a folder is clicked by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- spellcheck the subject when 'allowSpellcheck' setting is true by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section) by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
+- Show unseen message count when the message list is threaded
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- in mobile mode hide folders(left) panel when a folder is clicked
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- spellcheck the subject when 'allowSpellcheck' setting is true
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section)
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- Browser support for autocompleting TOTP code
[#1251](https://github.com/the-djmaze/snappymail/issues/1251)
@@ -465,10 +849,10 @@
[#1199](https://github.com/the-djmaze/snappymail/issues/1199)
- Draft code to improve mobile breakpoints
[#1150](https://github.com/the-djmaze/snappymail/issues/1150)
-- address input: space character can trigger '_parseValue' if the email address looks complete by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
+- address input: space character can trigger '_parseValue' if the email address looks complete
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
### Fixed
- CHARSET is not valid in UTF8 mode
@@ -481,39 +865,39 @@
[#1220](https://github.com/the-djmaze/snappymail/issues/1220)
- "Request failed" on message move
[#1220](https://github.com/the-djmaze/snappymail/issues/1220)
-- Unwrapped text nodes attached to squire._root by @SergeyMosin
- [#1234](https://github.com/the-djmaze/snappymail/pull/1234)
+- Unwrapped text nodes attached to squire._root
+ [#1234](https://github.com/the-djmaze/snappymail/pull/1234) by @SergeyMosin
- Extra wrapper div is added in Squire every time a Draft is open (or closed) after save.
[#1208](https://github.com/the-djmaze/snappymail/issues/1208)
- foreach() argument must be of type array|object
[#1237](https://github.com/the-djmaze/snappymail/issues/1237)
-- `` tag 'style' is lost in replies by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- unseen indicator is not shown in thread view when 'listGrouped' settings is false by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
+- `` tag 'style' is lost in replies
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- unseen indicator is not shown in thread view when 'listGrouped' settings is false
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- TOTP plugin is dependent on ctype
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)
### Nextcloud
-- iFrame mode: click on unified search result opens inner iFrame by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened. by @SergeyMosin
- [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
-- Improve UX of "Put in Calendar" option in plugin by @theronakpatel
- [#1259](https://github.com/the-djmaze/snappymail/pull/1259)
+- iFrame mode: click on unified search result opens inner iFrame
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened
+ [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
+- Improve UX of "Put in Calendar" option in plugin
+ [#1259](https://github.com/the-djmaze/snappymail/pull/1259) by @theronakpatel
## 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)
+- application.ini msg_default_action
+ [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
+- application.ini view_show_next_message
+ [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
+- application.ini view_images
+ [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
+- nextcloud add ability to include custom php file in InstallStep migration
+ [#1197](https://github.com/the-djmaze/snappymail/pull/1197) by @SergeyMosin
- Support plugin for Squire editor
[#1192](https://github.com/the-djmaze/snappymail/issues/1192)
@@ -521,14 +905,14 @@
- 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/issues/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)
+- New bidi buttons to Squire editor
+ [#1200](https://github.com/the-djmaze/snappymail/pull/1200) by @rezaei92
### Fixed
- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT
[#1205](https://github.com/the-djmaze/snappymail/issues/1205)
-- 'reloadTime' function result is passed into 'setInterval' instead of the function by @SergeyMosin
- [#1204](https://github.com/the-djmaze/snappymail/pull/1204)
+- 'reloadTime' function result is passed into 'setInterval' instead of the function
+ [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- UNKNOWN-CTE Invalid data in MIME part
[#1186](https://github.com/the-djmaze/snappymail/issues/1186)
@@ -591,10 +975,10 @@
### Changed
- Optical issue with input fields for mail and folder search
[#1149](https://github.com/the-djmaze/snappymail/issues/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)
+- Update Chinese translation
+ [#1157](https://github.com/the-djmaze/snappymail/pull/1157) by @mayswind
+- Update Polish translation
+ [#1156](https://github.com/the-djmaze/snappymail/pull/1156) by @tinola
### Fixed
- Undefined SIG constants
@@ -623,18 +1007,18 @@
- Allow empty message body when there are attachments
[#1052](https://github.com/the-djmaze/snappymail/issues/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)
+- Update Portuguese
+ [#1124](https://github.com/the-djmaze/snappymail/pull/1124) by @ner00
+- Update Traditional Chinese (Taiwan)
+ [#1107](https://github.com/the-djmaze/snappymail/pull/1107) by @chiyi4488
+- Update Russian
+ [#1108](https://github.com/the-djmaze/snappymail/pull/1108) by @konkere
+- Update Italian
+ [#1094](https://github.com/the-djmaze/snappymail/pull/1094) by @cm-schl
+- Update French
+ [#1102](https://github.com/the-djmaze/snappymail/pull/1102) by @hguilbert
+- Update German
+ [#1087](https://github.com/the-djmaze/snappymail/pull/1087) by @cm-schl
### Fixed
- Show messagelist timeout/abort error for
@@ -695,8 +1079,8 @@
- Workaround Upgrade with Nextcloud stuck in Maintenance mode
[#1046](https://github.com/the-djmaze/snappymail/issues/1046)
- Use Actions()->decodeRawKey()
-- French language updated by @hguilbert
- [#1045](https://github.com/the-djmaze/snappymail/pull/1045)
+- French language updated
+ [#1045](https://github.com/the-djmaze/snappymail/pull/1045) by @hguilbert
- Sorting in folders not working
[#1022](https://github.com/the-djmaze/snappymail/issues/1022)
- Allow saving draft with empty body
@@ -760,15 +1144,15 @@
### Changed
- Improved FolderCollection handling
- MODSEQ requires 64-bit int
-- Update russian language by @Akrobs
- [#994](https://github.com/the-djmaze/snappymail/pull/994)
+- Update russian language
+ [#994](https://github.com/the-djmaze/snappymail/pull/994) by @Akrobs
- Don't make font bigger when screen > 1400px
- Put top menu "accountPlace" inside top-system-dropdown-id
- Put attachment controls inside attachmentsPlace
- Show message toolbar on screens > 1400px
[#970](https://github.com/the-djmaze/snappymail/issues/970)
-- Chinese updated by @mayswind
- [#1011](https://github.com/the-djmaze/snappymail/pull/1011)
+- Chinese updated
+ [#1011](https://github.com/the-djmaze/snappymail/pull/1011) by @mayswind
- Prevent folder/messages flags conflict by using the right name `attributes` for Folders
- FolderInformation() use jsonSerialize()
- Squire space handling on paste use `\u00A0` instead of ` ` for
@@ -811,16 +1195,16 @@
### Changed
- Reduce/simplify CSS footprint
-- Use the System/Browser font by default by @HeySora
- [#988](https://github.com/the-djmaze/snappymail/pull/988)
+- Use the System/Browser font by default
+ [#988](https://github.com/the-djmaze/snappymail/pull/988) by @HeySora
- Make layout fully responsive using matchMedia('(max-width: 799px)')
- Move brotli and gzip compress option to application.ini
- After page refreshes in background whilst editing a draft, the space bar stops working
[#860](https://github.com/the-djmaze/snappymail/issues/860)
-- Updated Portuguese by @ner00
- [#984](https://github.com/the-djmaze/snappymail/pull/984)
-- Updated French by @hguilbert
- [#985](https://github.com/the-djmaze/snappymail/pull/985)
+- Updated Portuguese
+ [#984](https://github.com/the-djmaze/snappymail/pull/984) by @ner00
+- Updated French
+ [#985](https://github.com/the-djmaze/snappymail/pull/985) by @hguilbert
### Fixed
- unset border-box for message body
@@ -916,8 +1300,8 @@
- Workaround "server connection error"
[#936](https://github.com/the-djmaze/snappymail/issues/936)
- Cleanup AbstractViewPopup handling
-- Italian translation by @lota
- [#948](https://github.com/the-djmaze/snappymail/pull/948)
+- Italian translation
+ [#948](https://github.com/the-djmaze/snappymail/pull/948) by @lota
- Replace Element.fromHTML() with createElement()
### Fixed
@@ -965,8 +1349,8 @@
## 2.25.5 – 2023-02-09
### Added
-- New dark themes by @TheCuteFoxxy
- [#925](https://github.com/the-djmaze/snappymail/pull/925)
+- New dark themes
+ [#925](https://github.com/the-djmaze/snappymail/pull/925) by @TheCuteFoxxy
- External images option 'Always when DKIM is valid' for
[#201](https://github.com/the-djmaze/snappymail/issues/201)
- Image whitelist menu on message for
@@ -1006,8 +1390,8 @@
### Added
- message maximum quoted text level for speed.
[#902](https://github.com/the-djmaze/snappymail/issues/902)
-- LoveDark Theme by @TheCuteFoxxy
- [#913](https://github.com/the-djmaze/snappymail/pull/913)
+- LoveDark Theme
+ [#913](https://github.com/the-djmaze/snappymail/pull/913) by @TheCuteFoxxy
### Changed
- Moved pluginEnable() to \SnappyMail\Repository::enablePackage()
@@ -1089,8 +1473,8 @@
[#188](https://github.com/the-djmaze/snappymail/issues/188)
- /static/.htaccess as mentioned by @dbiczo in
[#895](https://github.com/the-djmaze/snappymail/issues/895)
-- Update nb-NO language by @master3395
- [#896](https://github.com/the-djmaze/snappymail/pull/896)
+- Update nb-NO language
+ [#896](https://github.com/the-djmaze/snappymail/pull/896) by @master3395
- \RainLoop\Utils::SetCookie() to \SnappyMail\Cookies::set()
- Merge \MailSo\Base\Utils::RecRmDir() and \MailSo\Base\Utils::RecTimeDirRemove()
@@ -1115,8 +1499,8 @@
- Support RFC 8689
- Nextcloud move themes outside of app folder
[#875](https://github.com/the-djmaze/snappymail/issues/875)
-- Add check to unregister system addressbook by @akhil1508
- [#879](https://github.com/the-djmaze/snappymail/pull/879)
+- Add check to unregister system addressbook
+ [#879](https://github.com/the-djmaze/snappymail/pull/879) by @akhil1508
### Changed
- material-design checkbox use hidden input for accessibility
@@ -1140,8 +1524,8 @@
## 2.25.0 – 2023-01-26
### Changed
-- Right margin to folder search input wrapper by @codiflow
- [#871](https://github.com/the-djmaze/snappymail/pull/871)
+- Right margin to folder search input wrapper
+ [#871](https://github.com/the-djmaze/snappymail/pull/871) by @codiflow
- Almost all request params now use JavaScript camelCase instead of CamelCase
- #rl-content to use flex for better layout control
- Decode some Copernica link tracking
@@ -1170,8 +1554,8 @@
### Added
- Preparations for RFC 8689
- Add admin setting for the contacts suggestions limit
-- A button to clear folder search input field by @codiflow
- [#847](https://github.com/the-djmaze/snappymail/pull/847)
+- A button to clear folder search input field
+ [#847](https://github.com/the-djmaze/snappymail/pull/847) by @codiflow
- Preparations for custom SMTP credentials handling for
[#859](https://github.com/the-djmaze/snappymail/issues/859)
[#458](https://github.com/the-djmaze/snappymail/issues/458)
@@ -1187,8 +1571,8 @@
[#849](https://github.com/the-djmaze/snappymail/issues/849)
- 64-bit PHP was required
[#852](https://github.com/the-djmaze/snappymail/issues/852)
-- Update fr-FR translations by @dominiquefournier
- [#854](https://github.com/the-djmaze/snappymail/pull/854)
+- Update fr-FR translations
+ [#854](https://github.com/the-djmaze/snappymail/pull/854) by @dominiquefournier
- Move createDomain & createDomainAlias buttons below table
- Move some Nextcloud SnappyMailHelper code to new InstallStep
- Cleanup messages cache handling
@@ -1214,8 +1598,8 @@
### Added
- Support for search criterias ON, SENTON, SENTSINCE and SENTBEFORE
- New 'forward as attachment' glyph
-- NC integration app metadata: add links to admin and dev docs by @p-bo
- [#820](https://github.com/the-djmaze/snappymail/pull/820)
+- NC integration app metadata: add links to admin and dev docs
+ [#820](https://github.com/the-djmaze/snappymail/pull/820) by @p-bo
- Clicking on messagelist grouped date/from searches on that date/from
[#815](https://github.com/the-djmaze/snappymail/issues/815)
- Nextcloud Improved file picker layout
@@ -1229,16 +1613,16 @@
- Resolve layout issues by changing from `fixed` to `relative`
[#686](https://github.com/the-djmaze/snappymail/issues/686)
- Cache handling of messagelist changed due to etag issues
-- Improve german and italian translation by @cm-schl
- [#846](https://github.com/the-djmaze/snappymail/pull/846)
+- Improve german and italian translation
+ [#846](https://github.com/the-djmaze/snappymail/pull/846) by @cm-schl
### Fixed
- Searching on Nextcloud search
[#787](https://github.com/the-djmaze/snappymail/issues/787)
- Workaround another Nextcloud disallowed .htaccess
[#790](https://github.com/the-djmaze/snappymail/issues/790)
-- Compact display of folders in nextcloud by @makoehr
- [#824](https://github.com/the-djmaze/snappymail/pull/824)
+- Compact display of folders in nextcloud
+ [#824](https://github.com/the-djmaze/snappymail/pull/824) by @makoehr
- Admin -> Contacts PDO test failed when using different AddressBookInterface
- KolabAddressBook errors
- Forgot debug js/css setting change in Nextcloud
@@ -1290,8 +1674,8 @@
### Changed
- Disable sorting when viewing message thread
[#445](https://github.com/the-djmaze/snappymail/issues/445)
-- Update Chinese translation by @mayswind
- [#794](https://github.com/the-djmaze/snappymail/pull/794)
+- Update Chinese translation
+ [#794](https://github.com/the-djmaze/snappymail/pull/794) by @mayswind
- No need to call IMAP EXAMINE when current folder already SELECT
- Thread view now has tree indentation
@@ -1404,8 +1788,8 @@
### Fixed
- Loosing HTML signature in account identity under settings
[#750](https://github.com/the-djmaze/snappymail/issues/750)
-- Plugin configuration did not load anymore when type was SELECTION by @cm-schl
- [#753](https://github.com/the-djmaze/snappymail/pull/753)
+- Plugin configuration did not load anymore when type was SELECTION
+ [#753](https://github.com/the-djmaze/snappymail/pull/753) by @cm-schl
- Nextcloud Default theme shows gray text on gray background
[#754](https://github.com/the-djmaze/snappymail/issues/754)
- Only run JSON hooks when $sAction is set
@@ -1669,8 +2053,8 @@
[#638](https://github.com/the-djmaze/snappymail/issues/638)
### Fixed
-- ldap-identities-plugin by @cm-schl
- [#647](https://github.com/the-djmaze/snappymail/pull/647)
+- ldap-identities-plugin
+ [#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
- OpenSSL v3 ciphers issue
[#641](https://github.com/the-djmaze/snappymail/issues/641)
@@ -1769,8 +2153,8 @@
[#620](https://github.com/the-djmaze/snappymail/issues/620)
- PutinICS does is not working for all calendar events
[#622](https://github.com/the-djmaze/snappymail/issues/622)
-- Update readme by @cm-schl
- [#617](https://github.com/the-djmaze/snappymail/pull/617)
+- Update readme
+ [#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
## 2.20.0 – 2022-11-03
@@ -1864,8 +2248,8 @@
[#638](https://github.com/the-djmaze/snappymail/issues/638)
### Fixed
-- ldap-identities-plugin by @cm-schl
- [#647](https://github.com/the-djmaze/snappymail/pull/647)
+- ldap-identities-plugin
+ [#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
- OpenSSL v3 ciphers issue
[#641](https://github.com/the-djmaze/snappymail/issues/641)
@@ -1964,8 +2348,8 @@
[#620](https://github.com/the-djmaze/snappymail/issues/620)
- PutinICS does is not working for all calendar events
[#622](https://github.com/the-djmaze/snappymail/issues/622)
-- Update readme by @cm-schl
- [#617](https://github.com/the-djmaze/snappymail/pull/617)
+- Update readme
+ [#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
## 2.20.0 – 2022-11-03
diff --git a/README.md b/README.md
index e1f64010e..b9736d393 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ This fork of RainLoop has the following changes:
* Admin uses password_hash/password_verify
* Auth failed attempts written to syslog
* Added Fail2ban instructions
-* ES2018
+* ES2020
* PHP 7.4+ required
* PHP mbstring extension required
* PHP replaced pclZip with PharData and ZipArchive
@@ -66,7 +66,7 @@ This fork of RainLoop has the following changes:
* CRLF => LF line endings
* Embed boot.js and boot.css into index.html
* Removal of old JavaScript code (things are native these days)
-* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
+* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
* Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
@@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
-|admin.js |2.170.153 | 82.845 |
-|app.js |4.207.787 | 430.201 |
-|boot.js | 868.735 | 4.147 |
-|libs.js | 658.812 | 201.911 |
-|sieve.js | 0 | 84.703 |
+|admin.js |2.170.153 | 84.925 |
+|app.js |4.207.787 | 447.263 |
+|boot.js | 868.735 | 4.343 |
+|libs.js | 658.812 | 233.728 |
+|sieve.js | 0 | 91.418 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
-|TOTAL |8.240.095 | 804.092 |
+|TOTAL |8.240.095 | 861.962 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
-|admin.min.js | 256.831 | 40.733 | 73.606 | 13.649 | 60.877 | 12.228 |
-|app.min.js | 515.367 | 195.260 |139.456 | 66.275 |110.485 | 56.682 |
-|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
-|libs.min.js | 584.772 | 92.482 |180.901 | 34.729 |155.182 | 31.017 |
-|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
+|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
+|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
+|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
+|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
+|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
-|TOTAL user |1.217.635 | 289.829 |358.761 |102.208 |299.485 | 88.701 |
-|TOTAL user+sieve |1.217.635 | 330.993 |358.761 |112.573 |299.485 | 98.060 |
-|TOTAL admin | 959.099 | 135.302 |292.911 | 49.582 |249.877 | 44.247 |
+|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
+|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
+|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
-For a user it is around 69% smaller and faster than traditional RainLoop.
+For a user it is around 66% smaller and faster than traditional RainLoop.
### CSS changes
@@ -188,12 +188,12 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
-|app.css | 340.331 | 84.531 | 46.946 | 17.627 | 15.111 |
-|app.min.css | 274.947 | 67.906 | 39.647 | 15.544 | 13.567 |
+|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
+|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
-|admin.css | | 30.573 | | 7.011 | 6.101 |
-|admin.min.css | | 24.689 | | 6.334 | 5.575 |
+|admin.css | | 30.880 | | 7.045 | 6.127 |
+|admin.min.css | | 24.959 | | 6.368 | 5.615 |
### PGP
RainLoop uses the old OpenPGP.js v2
@@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
-|openpgp.min.js | 330.742 | 546.309 |102.388 | 169.249 | 84.241 | 138.751 |
+|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|openpgp.worker | 1.499 | | 824 | | 695 | |
diff --git a/SECURITY.md b/SECURITY.md
index 1b43a988f..11d1ddada 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -6,8 +6,8 @@ Currently due to the fast development only the latest version receives security
| Version | Supported |
| -------- | --------- |
-| 2.30.x | ✔ |
-| < 2.30.0 | ❌ |
+| 2.34.x | ✔ |
+| < 2.34.0 | ❌ |
## Reporting a Vulnerability
@@ -36,5 +36,5 @@ YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
## Publishing and Credits
I will analyze and fix the reported issue as fast as possible.
-Together with the reporter I plan the disclosure of the found and fixed vulnerability.
+Together with the reporter, I plan the disclosure of the found and fixed vulnerability.
Credits to the reporter are granted and can be included in all public communication if desired.
diff --git a/cli/release.php b/cli/release.php
index aac915b02..fe406f7f3 100755
--- a/cli/release.php
+++ b/cli/release.php
@@ -9,7 +9,7 @@ if (isset($options['plugins'])) {
require(ROOT_DIR . '/build/plugins.php');
}
-$gulp = trim(`which gulp`);
+$gulp = trim(shell_exec('which gulp'));
if (!$gulp) {
exit('gulp not installed, run as root: npm install --global gulp-cli');
}
@@ -34,7 +34,7 @@ $options['aur'] = isset($options['aur']);
// Docker build
$options['docker'] = isset($options['docker']);
-if ($options['docker'] && !($docker = trim(`which docker`))) {
+if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
exit('docker not found');
}
if ($options['docker'] && $options['aur']) {
@@ -60,7 +60,7 @@ if (!isset($options['skip-gulp'])) {
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
- if ($gzip = trim(`which gzip`)) {
+ if ($gzip = trim(shell_exec('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");
@@ -70,7 +70,7 @@ if (!isset($options['skip-gulp'])) {
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
}
- if ($brotli = trim(`which brotli`)) {
+ if ($brotli = trim(shell_exec('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");
@@ -188,7 +188,7 @@ file_put_contents("{$destPath}core.json", '{
// Arch User Repository
if ($options['aur']) {
// extension_loaded('blake2')
- if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) {
+ if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
function b2sum($file) {
$file = escapeshellarg($file);
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
diff --git a/dev/App/User.js b/dev/App/User.js
index 5e8d94e3b..5e28164ac 100644
--- a/dev/App/User.js
+++ b/dev/App/User.js
@@ -1,7 +1,7 @@
import 'External/User/ko';
import { SMAudio } from 'Common/Audio';
-import { pInt } from 'Common/Utils';
+import { arrayLength } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
import {
@@ -27,7 +27,7 @@ import {
getFolderFromCacheList
} from 'Common/Cache';
-import { i18n, reloadTime } from 'Common/Translator';
+import { i18n, reloadTime, getErrorMessage } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { NotificationUserStore } from 'Stores/User/Notification';
@@ -88,6 +88,8 @@ export class AppUser extends AbstractApp {
this.messageList = MessagelistUserStore;
this.ask = AskPopupView;
+
+ this.loadAccountsAndIdentities = loadAccountsAndIdentities;
}
/**
@@ -112,7 +114,7 @@ export class AppUser extends AbstractApp {
case FolderType.Trash:
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
nSetSystemFoldersNotification = iFolderType;
- bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
+ bDelete = bDelete/* || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()*/
|| sFromFolderFullName === FolderUserStore.spamFolder()
|| sFromFolderFullName === FolderUserStore.trashFolder();
break;
@@ -147,8 +149,10 @@ export class AppUser extends AbstractApp {
}
logout() {
- localStorage.removeItem('register_protocol_offered');
- Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
+ Remote.request('Logout', (iError, data) =>
+ iError ? alert('Logout error: ' + getErrorMessage(iError, data))
+ : rl.logoutReload(Settings.app('customLogoutLink'))
+ );
}
bootstart() {
@@ -180,15 +184,15 @@ export class AppUser extends AbstractApp {
SettingsUserStore.init();
ContactUserStore.init();
- loadFolders(value => {
+ loadFolders((success, error) => {
try {
- if (value) {
+ if (success) {
startScreens([
MailBoxUserScreen,
SettingsUserScreen
]);
- setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
+ setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
loadAccountsAndIdentities();
@@ -222,19 +226,9 @@ export class AppUser extends AbstractApp {
SMimeUserStore.loadCertificates();
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
-
- if (!localStorage.getItem('register_protocol_offered')) {
- // When auto-login is active
- navigator.registerProtocolHandler?.(
- 'mailto',
- location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
- (SettingsGet('title') || 'SnappyMail')
- );
- localStorage.setItem('register_protocol_offered', '1');
- }
-
} else {
this.logout();
+ alert('Folders error: ' + getErrorMessage(0, error))
}
} catch (e) {
console.error(e);
@@ -247,18 +241,31 @@ export class AppUser extends AbstractApp {
showMessageComposer(params = [])
{
- showScreenPopup(ComposePopupView, params);
+ let msg = params[1];
+ if (1 == arrayLength(msg)) {
+ msg = msg[0];
+ }
+ if (msg) {
+ msg.decrypt().then((/*success*/)=>showScreenPopup(ComposePopupView, params));
+ } else {
+ showScreenPopup(ComposePopupView, params);
+ }
}
}
-AskPopupView.password = function(sAskDesc, btnText) {
+AskPopupView.password = function(sAskDesc, btnText, ask) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
- view => resolve({password:view.passphrase(), remember:view.remember()}),
+ view => resolve({
+ password:view.passphrase(),
+ username:/*ask & 2 ? */view.username(),
+ remember:/*ask & 4 ? */view.remember(),
+ rememberPermanent: view.rememberPermanent(),
+ }),
() => resolve(null),
true,
- 5,
+ ask || 1,
btnText
]);
});
diff --git a/dev/Common/EnumsUser.js b/dev/Common/EnumsUser.js
index bc3c873ba..a8948e5a7 100644
--- a/dev/Common/EnumsUser.js
+++ b/dev/Common/EnumsUser.js
@@ -71,7 +71,9 @@ MessageSetAction = {
SetSeen: 0,
UnsetSeen: 1,
SetFlag: 2,
- UnsetFlag: 3
+ UnsetFlag: 3,
+ SetDeleted: 4,
+ UnsetDeleted: 5
},
/**
diff --git a/dev/Common/File.js b/dev/Common/File.js
index 841554056..38b1f917b 100644
--- a/dev/Common/File.js
+++ b/dev/Common/File.js
@@ -1,7 +1,7 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
-import { arrayLength } from 'Common/Utils';
+import { arrayLength, pInt } from 'Common/Utils';
export const RFC822 = 'message/rfc822';
@@ -274,8 +274,7 @@ export const FileInfo = {
},
friendlySize: bytes => {
- bytes = parseInt(bytes, 10);
- bytes = isFinite(bytes) ? bytes : 0;
+ bytes = pInt(bytes);
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0;
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
}
diff --git a/dev/Common/Folders.js b/dev/Common/Folders.js
index cbba0390d..a6278201c 100644
--- a/dev/Common/Folders.js
+++ b/dev/Common/Folders.js
@@ -1,6 +1,8 @@
import { RFC822 } from 'Common/File';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
-import { isArray, arrayLength } from 'Common/Utils';
+import { baseCollator } from 'Common/Translator';
+import { SettingsGet } from 'Common/Globals';
+import { isArray, arrayLength, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
@@ -8,13 +10,13 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
let refreshInterval,
- // Default every 5 minutes
- refreshFoldersInterval = 300000;
+ // Default every 15 minutes
+ refreshFoldersInterval = 900000;
export const
setRefreshFoldersInterval = minutes => {
- refreshFoldersInterval = Math.max(5, minutes) * 60000;
+ refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(),
@@ -27,7 +29,7 @@ setRefreshFoldersInterval = minutes => {
sortFolders = folders => {
try {
- let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
+ let collator = baseCollator(true);
folders.sort((a, b) =>
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
);
@@ -197,7 +199,7 @@ dropFilesInFolder = (sFolderFullName, files) => {
data.append('folder', sFolderFullName);
data.append('appendFile', file);
Remote.request('FolderAppend', (iError, data)=>{
- iError && console.error(data.ErrorMessage);
+ iError && console.error(data.message);
0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true);
diff --git a/dev/Common/Html.js b/dev/Common/Html.js
index 27e59a2e1..072045f9a 100644
--- a/dev/Common/Html.js
+++ b/dev/Common/Html.js
@@ -4,6 +4,9 @@ import { SettingsUserStore } from 'Stores/User/Settings';
const
tmpl = createElement('template'),
+
+ turndown = new TurndownService(),
+
htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i,
htmlmap = {
@@ -14,14 +17,34 @@ const
"'": '''
},
- disallowedTags = [
- 'svg','script','title','link','base','meta',
- 'input','output','select','button','textarea',
- 'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
- // not supported by element
-// ,'html','head','body'
+ keepTagContent = 'form,button,data', // font
+
+ allowedTags = [
+ // Structural Elements:
+ 'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
+ 'article','aside','header','footer','main','section',
+ 'details','summary','nav',
+ // List Elements
+ 'dd','dl','dt','li','ol','ul',
+ // Text Formatting Elements
+ 'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
+ 'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
+ 'span','strong','sub','sup','time','u','var',
+ // Deprecated by HTML Standard
+ 'acronym','big','center','dir','font','marquee',
+ 'nobr','plaintext','rb','rtc','strike','tt',
+ // Media Elements
+ 'img',//'picture','source',
+ // Table Elements
+ 'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
+ // Disallowed but converted later
+ 'style','xmp'
].join(','),
+ nonEmptyTags = [
+ 'A','B','EM','I','SPAN','STRONG'
+ ],
+
blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() &&
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
@@ -99,8 +122,10 @@ const
},
cleanCSS = source =>
- source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
- .replace(/white-space[^;]+(;|$)/g, '')
+ source.trim()
+ .replace(/;\s*-[^;]+/g, '')
+ .replace(/^\s*-[^;]+(;|$)/g, '')
+ .replace(/white-space[^;]+/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
,
@@ -142,14 +167,15 @@ const
if (source) {
source = source
// strip comments
- .replace(/\/\*[\s\S]*?\*\/|/gi, '')
- // strip import statements
- .replace(/@import .*?;/gi , '')
- // strip keyframe statements
- .replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
+ .replace(/\/\*[\s\S]*?\*\//gi, '')
+ // strip MS Word comments
+ .replace(//gi, '')
+ // strip HTML, as < is no CSS combinator anyway
+ .replace(/<[\s\S]*/gi, '');
+// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
// unified regex to match css & media queries together
- let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
+ let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
arr;
while (true) {
@@ -158,7 +184,7 @@ const
break;
}
- let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
+ let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
// Never have more than a single line break in a row
.replace(/\n+/, "\n")
// Remove :root and html
@@ -170,13 +196,14 @@ const
css.push({
selector: selector,
type: 'media',
- subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
+ subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
});
} else if (selector && !selector.includes('@')) {
// we have standard css
+ // ignores @import, @keyframe, @font-face statements
css.push({
selector: selector,
- rules: cleanCSS(arr[6])
+ rules: cleanCSS(arr[4])
});
}
}
@@ -205,6 +232,7 @@ export const
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
+ minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = {
hasExternals: false,
@@ -212,6 +240,8 @@ export const
linkedData: []
},
+ isMsg = !!msgId,
+
findAttachmentByCid = cId => oAttachments.findByCid(cId),
findLocationByCid = cId => {
const attachment = findAttachmentByCid(cId);
@@ -228,6 +258,15 @@ export const
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
},
allowedAttributes = [
+ // SnappyMail
+// 'data-x-href-broken',
+// 'data-x-href-tracking',
+ 'data-x-src',
+// 'data-x-src-hidden',
+// 'data-x-src-tracking',
+// 'data-x-src-broken',
+// 'data-x-style-url',
+// 'data-x-style-broken-urls',
// defaults
'name',
'dir', 'lang', 'style', 'title',
@@ -255,9 +294,8 @@ export const
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
- ],
- nonEmptyTags = [
- 'A','B','EM','I','SPAN','STRONG'
+ // others
+ //'class', 'id', 'target'
];
if (SettingsUserStore.allowStyles()) {
@@ -271,6 +309,8 @@ export const
// Not supported by element
// .replace(/]*>/gi, '')
// .replace(/<\?xml[^>]*\?>/gi, '')
+ // https://github.com/the-djmaze/snappymail/issues/1860
+ .replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
// .replace(/<\/?(html|head)[^>]*>/gi, '')
@@ -304,13 +344,33 @@ export const
}
});
+ // https://github.com/the-djmaze/snappymail/issues/1125
+ tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
+
tmpl.content.querySelectorAll(
- disallowedTags
+ ':not('+allowedTags+'),a:empty,span:empty'
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove());
+/* // Is this slower or faster?
+ ).forEach(oElement => {
+ if (!node || !node.contains(oElement)) {
+ oElement.remove();
+ node = oElement;
+ }
+ });
+*/
- // https://github.com/the-djmaze/snappymail/issues/1125
- tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
+ // https://github.com/the-djmaze/snappymail/issues/1641
+ let body = tmpl.content.querySelector('.mail-body');
+ [...tmpl.content.querySelectorAll('.mail-body + .mail-body')]
+ .forEach(oElement => body.append(...oElement.childNodes));
+/*
+ .forEach(oElement => {
+ let bq = createElement('blockquote');
+ bq.append(...oElement.childNodes);
+ body.replaceWith(bq);
+ });
+*/
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName,
@@ -331,6 +391,13 @@ export const
return;
}
+ if ('XMP' === name) {
+ const pre = createElement('pre');
+ pre.innerHTML = encodeHtml(oElement.innerHTML);
+ oElement.replaceWith(pre);
+ return;
+ }
+
// \MailSo\Base\HtmlUtils::ClearTags()
if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility
@@ -417,7 +484,7 @@ export const
}
// if (['CENTER','FORM'].includes(name)) {
- if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
+ if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return;
}
@@ -430,70 +497,73 @@ export const
*/
let skipStyle = false;
- value = delAttribute('src');
- if (value) {
- if ('IMG' === name) {
- oElement.loading = 'lazy';
- let attachment;
- if (value.startsWith('cid:'))
- {
- value = value.slice(4);
- setAttribute('data-x-src-cid', value);
- attachment = findAttachmentByCid(value);
- if (attachment?.download) {
- oElement.src = attachment.linkPreview();
- oElement.title += ' ('+attachment.fileName+')';
- attachment.isInline(true);
- attachment.isLinked(true);
+ if (isMsg) {
+ value = isMsg && delAttribute('src');
+ if (value) {
+ if ('IMG' === name) {
+ oElement.loading = 'lazy';
+ let attachment;
+ if (value.startsWith('cid:'))
+ {
+ setAttribute('data-x-src', value);
+ value = value.slice(4);
+ attachment = findAttachmentByCid(value);
+ if (attachment?.download) {
+ oElement.src = attachment.linkPreview();
+// oElement.title || (oElement.title = '+attachment.fileName);
+ attachment.isInline(true);
+ attachment.isLinked(true);
+ }
}
- }
- else if ((attachment = findLocationByCid(value)))
- {
- if (attachment.download) {
- oElement.src = attachment.linkPreview();
- attachment.isLinked(true);
+ else if ((attachment = findLocationByCid(value)))
+ {
+ if (attachment.download) {
+ oElement.src = attachment.linkPreview();
+ attachment.isLinked(true);
+ }
}
- }
- else if (detectHiddenImages
- && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
- || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
- || [
- 'email.microsoftemail.com/open',
- 'github.com/notifications/beacon/',
- '/track/open', // mandrillapp.com list-manage.com
- 'google-analytics.com'
- ].filter(uri => value.toLowerCase().includes(uri)).length
- )) {
- skipStyle = true;
- oStyle.display = 'none';
-// setAttribute('style', 'display:none');
- setAttribute('data-x-src-hidden', value);
-// result.tracking = true;
- }
- else if (httpre.test(value))
- {
- let src = stripTracking(value);
- if (src != value) {
- result.tracking = true;
- setAttribute('data-x-src-tracking', value);
+ else if (detectHiddenImages
+ && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
+ || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
+ || (oStyle.width && 2 > pInt(oStyle.width))
+ || [
+ 'email.microsoftemail.com/open',
+ 'github.com/notifications/beacon/',
+ '/track/open', // mandrillapp.com list-manage.com
+ 'google-analytics.com'
+ ].filter(uri => value.toLowerCase().includes(uri)).length
+ )) {
+ skipStyle = true;
+ oStyle.display = 'none';
+ // setAttribute('style', 'display:none');
+ setAttribute('data-x-src-hidden', value);
+ // result.tracking = true;
+ }
+ else if (httpre.test(value))
+ {
+ let src = stripTracking(value);
+ if (src != value) {
+ result.tracking = true;
+ setAttribute('data-x-src-tracking', value);
+ }
+ setAttribute('data-x-src', src);
+ result.hasExternals = true;
+ oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
+ }
+ else if (value.startsWith('data:image/'))
+ {
+ oElement.src = value;
+ }
+ else
+ {
+ setAttribute('data-x-src-broken', value);
}
- setAttribute('data-x-src', src);
- result.hasExternals = true;
- oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
- }
- else if (value.startsWith('data:image/'))
- {
- oElement.src = value;
}
else
{
setAttribute('data-x-src-broken', value);
}
}
- else
- {
- setAttribute('data-x-src-broken', value);
- }
}
if (hasAttribute('background')) {
@@ -556,12 +626,13 @@ export const
if (urls_broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
}
-/*
+
// https://github.com/the-djmaze/snappymail/issues/1082
- if (11 > pInt(oStyle.fontSize)) {
- oStyle.removeProperty('font-size');
+ if (minFontSize > pInt(oStyle.fontSize)) {
+ oStyle.fontSize = minFontSize + "px";
+// oStyle.removeProperty('font-size');
}
-*/
+
// Removes background and color
// Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability
@@ -579,7 +650,7 @@ export const
}
});
- blockquoteSwitcher();
+ isMsg && blockquoteSwitcher();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
@@ -591,6 +662,9 @@ export const
* @returns {string}
*/
htmlToPlain = html => {
+ if (SettingsUserStore.markdown()) {
+ return htmlToMarkdown(html);
+ }
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
@@ -611,6 +685,8 @@ export const
html = html
.replace(/]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '
') : '')
+ // Remove line duplication
+ .replace(/
<\/div>/gi, '')
.replace(/\r?\n/g, '')
.replace(/\s+/gm, ' ');
@@ -625,6 +701,8 @@ export const
.replace(//gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
+ forEach('style', node => node.remove());
+
// lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
@@ -661,7 +739,9 @@ export const
forEach('a', a => {
let txt = a.textContent, href = a.href;
return a.replaceWith(
- txt.trim() == href || href.includes('mailto:') ? txt : txt + ' ' + href + ' '
+ txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, ''))
+ ? txt
+ : txt + ' ' + href + ' '
);
});
@@ -683,6 +763,11 @@ export const
return (tmpl.content.textContent || '').trim();
},
+ htmlToMarkdown = html => {
+ tmpl.innerHTML = html;
+ return turndown.turndown(tmpl.content);
+ },
+
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
@@ -753,6 +838,9 @@ export const
};
rl.Utils = {
+ cleanHtml: cleanHtml,
htmlToPlain: htmlToPlain,
- plainToHtml: plainToHtml
+ plainToHtml: plainToHtml,
+ htmlToMarkdown: htmlToMarkdown
+// markdownToHtml: md => marked.parse(md)
};
diff --git a/dev/Common/Links.js b/dev/Common/Links.js
index f8847b95f..c0923ed58 100644
--- a/dev/Common/Links.js
+++ b/dev/Common/Links.js
@@ -38,14 +38,14 @@ export const
/**
* @param {string} download
- * @param {string=} customSpecSuffix
* @returns {string}
*/
- attachmentDownload = (download, customSpecSuffix) =>
- serverRequestRaw('Download', download, customSpecSuffix),
+ attachmentDownload = (download) =>
+ serverRequestRaw('Download', download),
proxy = url =>
BASE + '?/ProxyExternal/'
+// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
diff --git a/dev/Common/Translator.js b/dev/Common/Translator.js
index 4d1111a64..b4953291f 100644
--- a/dev/Common/Translator.js
+++ b/dev/Common/Translator.js
@@ -45,8 +45,7 @@ export const
}
}
if (Intl.RelativeTimeFormat) {
- let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
- return rtf.format(seconds, unit);
+ return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
}
// Safari < 14
abs = Math.abs(seconds);
@@ -62,7 +61,7 @@ export const
* @returns {string}
*/
i18n = (key, valueList, defaulValue) => {
- let result = null == defaulValue ? key : defaulValue;
+ let result = defaulValue ?? key;
let path = key.split('/');
if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result;
@@ -185,6 +184,9 @@ export const
|| '';
},
+ getErrorMessage = (code, data) =>
+ getNotification(code) || data?.messageAdditional || data?.message || data,
+
/**
* @param {*} code
* @returns {string}
@@ -218,7 +220,6 @@ export const
}),
/**
- *
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
@@ -228,6 +229,8 @@ export const
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
- );
+ ),
+
+ baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
init();
diff --git a/dev/Common/Utils.js b/dev/Common/Utils.js
index 0b51d4ba3..d4616f30d 100644
--- a/dev/Common/Utils.js
+++ b/dev/Common/Utils.js
@@ -10,7 +10,7 @@ export const
pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10);
- return isNaN(value) || !isFinite(value) ? defaultValue : value;
+ return isFinite(value) ? value : defaultValue;
},
defaultOptionsAfterRender = (domItem, item) =>
diff --git a/dev/Common/UtilsUser.js b/dev/Common/UtilsUser.js
index 0c683aa25..503cd793b 100644
--- a/dev/Common/UtilsUser.js
+++ b/dev/Common/UtilsUser.js
@@ -18,15 +18,21 @@ import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
import { isArray } from 'Common/Utils';
+import { showScreenPopup } from 'Knoin/Knoin';
+import { IdentityPopupView } from 'View/Popup/Identity';
+
export const
-moveAction = ko.observable(false),
+// 1 = move, 2 = copy
+moveAction = ko.observable(0),
dropdownsDetectVisibility = (() =>
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
).debounce(50),
+editIdentity = Identity => showScreenPopup(IdentityPopupView, [Identity]),
+
loadAccountsAndIdentities = () => {
AccountUserStore.loading(true);
IdentityUserStore.loading(true);
@@ -48,6 +54,11 @@ loadAccountsAndIdentities = () => {
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
: []
);
+
+ // Invoke "Update Identity" pop up right after login
+ // https://github.com/the-djmaze/snappymail/issues/1689
+ const main = IdentityUserStore.main();
+ main && !main.exists() && setTimeout(()=>editIdentity(main), 1000);
}
});
},
@@ -121,7 +132,7 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
next = 0,
limit = 2;
- if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
+ if (1 < pageCount) {
if (pageCount < currentPage) {
fAdd(pageCount);
prev = pageCount;
@@ -359,5 +370,5 @@ populateMessageBody = (oMessage, popup) => {
}
};
-leftPanelDisabled.subscribe(value => value && moveAction(false));
+leftPanelDisabled.subscribe(value => value && moveAction(0));
moveAction.subscribe(value => value && leftPanelDisabled(false));
diff --git a/dev/Component/Checkbox.js b/dev/Component/Checkbox.js
index 1f943d884..b0675cd2b 100644
--- a/dev/Component/Checkbox.js
+++ b/dev/Component/Checkbox.js
@@ -6,7 +6,7 @@ export class CheckboxComponent {
: ko.observable(!!params.value);
this.enable = ko.isObservable(params.enable) ? params.enable
- : ko.observable(undefined === params.enable || !!params.enable);
+ : ko.observable(params.enable ?? 1);
this.label = params.label;
}
diff --git a/dev/DAV/JCard.js b/dev/DAV/JCard.js
index 1dad7d8c8..ad0850df8 100644
--- a/dev/DAV/JCard.js
+++ b/dev/DAV/JCard.js
@@ -124,7 +124,7 @@ export class JCard {
// VCardProperty argument
else if (arg instanceof VCardProperty) {
let propArray = this.props.get(arg.getField());
- if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
+ if (!propArray?.includes(arg))
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
propArray.splice(propArray.indexOf(arg), 1);
if (propArray.length === 0)
diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js
index 374ea795f..b1e50fcad 100644
--- a/dev/External/SquireUI.js
+++ b/dev/External/SquireUI.js
@@ -3,10 +3,6 @@
(doc => {
const
- removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
- allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
- allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
-
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey() + ' + ',
@@ -21,38 +17,17 @@ const
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
- getFragmentOfChildren = parent => {
- let frag = doc.createDocumentFragment();
- frag.append(...parent.childNodes);
- return frag;
- },
-
SquireDefaultConfig = {
/*
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
- tpl.innerHTML = (html||'')
+ html = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(//g,'')
.replace(/]*>\s*<\/span>/gi,'')
.trim();
- tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
- if (isPaste) {
- tpl.querySelectorAll(removeElements).forEach(el => el.remove());
- tpl.querySelectorAll('*').forEach(el => {
- if (!el.matches(allowedElements)) {
- el.replaceWith(getFragmentOfChildren(el));
- } else if (el.hasAttributes()) {
- [...el.attributes].forEach(attr => {
- let name = attr.name.toLowerCase();
- if (!allowedAttributes.includes(name)) {
- el.removeAttribute(name);
- }
- });
- }
- });
- }
+ tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
return tpl.content;
}
};
@@ -109,13 +84,13 @@ class SquireUI
cmd: s => squire.setStyle({ fontFamily: s.value })
},
fontSize: {
- select: ['11px','13px','16px','20px','24px','30px'],
- defaultValueIndex: 2,
+ select: [[i18n('GLOBAL/DEFAULT'),''],'11px','13px','16px','20px','24px','30px'],
+ defaultValueIndex: 0,
cmd: s => squire.setStyle({ fontSize: s.value })
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
// example:
- // select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
- // defaultValueIndex: 3,
+ // select: ['','xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
+ // defaultValueIndex: 0,
},
// dir: {
// select: [
@@ -133,11 +108,11 @@ class SquireUI
dir: {
dir_ltr: {
html: '⁋',
- cmd: () => squire.bidi('ltr')
+ cmd: () => squire.setTextDirection('ltr')
},
dir_rtl: {
html: '¶',
- cmd: () => squire.bidi('rtl')
+ cmd: () => squire.setTextDirection('rtl')
}
},
colors: {
@@ -155,7 +130,7 @@ class SquireUI
html: 'B',
cmd: () => this.doAction('bold'),
key: 'B',
- matches: 'B,STRONT'
+ matches: 'B,STRONG'
},
italic: {
html: 'I',
@@ -237,7 +212,7 @@ class SquireUI
cmd: () => {
let node = squire.getSelectionClosest('IMG'),
src = prompt("Image", node?.src || "https://");
- src?.length ? squire.insertImage(src) : (node && squire.detach(node));
+ src?.length ? squire.insertImage(src) : node?.remove();
},
matches: 'IMG'
},
@@ -355,6 +330,7 @@ class SquireUI
input.append(option);
});
} else {
+ input.add(new Option(i18n('GLOBAL/DEFAULT'), ''));
Object.entries(cfg.select).forEach(([label, options]) => {
let group = createElement('optgroup');
group.label = label;
@@ -363,7 +339,7 @@ class SquireUI
option.style[action] = value;
group.append(option);
});
- input.append(group);
+ input.add(group);
});
}
ev = 'input';
@@ -405,9 +381,47 @@ class SquireUI
let changes = actions.changes;
changes.undo.input.disabled = changes.redo.input.disabled = true;
- squire.addEventListener('undoStateChange', state => {
- changes.undo.input.disabled = !state.canUndo;
- changes.redo.input.disabled = !state.canRedo;
+ squire.addEventListener('undoStateChange', e => {
+ changes.undo.input.disabled = !e.detail.canUndo;
+ changes.redo.input.disabled = !e.detail.canRedo;
+ });
+
+ squire.addEventListener('pasteImage', e => {
+ const items = e.detail.clipboardData.items;
+ let l = items.length;
+ while (l--) {
+ const item = items[l];
+ if (/^image\/(png|jpeg|webp)/.test(item.type)) {
+ let reader = new FileReader();
+ reader.onload = event => {
+ let img = createElement("img"),
+ canvas = createElement("canvas"),
+ ctx = canvas.getContext('2d');
+ img.onload = ()=>{
+ ctx.drawImage(img, 0, 0);
+ let width = img.width, height = img.height;
+ if (width > height) {
+ // Landscape
+ if (width > 1024) {
+ height = height * 1024 / width;
+ width = 1024;
+ }
+ } else if (height > 1024) {
+ // Portrait
+ width = width * 1024 / height;
+ height = 1024;
+ }
+ canvas.width = width;
+ canvas.height = height;
+ ctx.drawImage(img, 0, 0, width, height);
+ squire.insertHTML('
', true);
+ };
+ img.src = event.target.result;
+ }
+ reader.readAsDataURL(item.getAsFile());
+ break;
+ }
+ }
});
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
@@ -477,24 +491,31 @@ class SquireUI
// -----
- squire.addEventListener('pathChange', e => {
+ squire.addEventListener('pathChange', () => {
const squireRoot = squire.getRoot();
-
+ let range = squire.getSelection(),
+ collapsed = range.collapsed,
+ elm = collapsed ? range.endContainer : range?.commonAncestorContainer;
+ if (elm && !(elm instanceof Element)) {
+ elm = elm.parentElement;
+ }
forEachObjectValue(actions, entries => {
forEachObjectValue(entries, cfg => {
-// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches));
- cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot));
+ // Check if selection has a matching parent or contains a matching element
+ cfg.matches && cfg.input.classList.toggle('active', !!(elm && (
+ (!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node)))
+ || elm.closestWithin(cfg.matches, squireRoot)
+ )));
});
});
- if (e.element) {
+ if (elm) {
// try to find font-family and/or font-size and set "select" elements' values
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
let familySelectedIndex = defaultFontFamilyIndex;
- let elm = e.element;
let familyFound = false;
let sizeFound = false;
do {
@@ -526,10 +547,10 @@ class SquireUI
});
/*
squire.addEventListener('cursor', e => {
- console.dir({cursor:e.range});
+ console.dir({cursor:e.detail.range});
});
squire.addEventListener('select', e => {
- console.dir({select:e.range});
+ console.dir({select:e.detail.range});
});
*/
}
diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js
index 78dc965eb..2e951e576 100644
--- a/dev/External/User/ko.js
+++ b/dev/External/User/ko.js
@@ -97,7 +97,7 @@ Object.assign(ko.bindingHandlers, {
fValue.subscribe(fUpdateEditorValue);
- // ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+ // ko.addDisposeCallback(element, () => {
// });
}
}
diff --git a/dev/External/ko.js b/dev/External/ko.js
index 0d693148d..d783ae1df 100644
--- a/dev/External/ko.js
+++ b/dev/External/ko.js
@@ -15,6 +15,7 @@ export const
* based solely on the values of other observables in the application
*/
koComputable = fn => ko.computed(fn, {'pure':true}),
+// koObservable = value => ko.observable(value),
addObservablesTo = (target, observables) =>
forEachObjectEntry(observables, (key, value) =>
@@ -30,7 +31,7 @@ export const
onEvent = (element, event, fn) => {
element.addEventListener(event, fn);
- ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
+ ko.addDisposeCallback(element, () => element.removeEventListener(event, fn));
},
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
@@ -175,6 +176,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions
-ko.observable.fn.askDeleteHelper = function() {
- return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
+ko.observable.fn.askDeleteHelper = function(prop='askDelete') {
+ return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, prop] });
};
diff --git a/dev/Html/PreviewMessage.html b/dev/Html/PreviewMessage.html
deleted file mode 100644
index 0aa25cfd0..000000000
--- a/dev/Html/PreviewMessage.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/dev/Knoin/AbstractViews.js b/dev/Knoin/AbstractViews.js
index 43ff02b26..409e44386 100644
--- a/dev/Knoin/AbstractViews.js
+++ b/dev/Knoin/AbstractViews.js
@@ -60,10 +60,9 @@ export class AbstractViewPopup extends AbstractView
this.keyScope.scope = name;
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
this.close = () => this.modalVisible(false);
+ this.tryToClose = () => (false === this.onClose()) || this.close();
addShortcut('escape,close', '', name, () => {
- if (this.modalVisible() && false !== this.onClose()) {
- this.close();
- }
+ this.modalVisible() && this.tryToClose();
return false;
// return true; Issue with supported modal close
});
@@ -116,6 +115,11 @@ export class AbstractViewSettings
onHide() {}
viewModelDom
*/
+ /**
+ * When this[name] does not exists, create as observable with value of SettingsGet(name)
+ * When this[name+'Trigger'] does not exists, create as observable
+ * Subscribe to this[name], and handle saving the setting
+ */
addSetting(name, valueCb)
{
let prop = name[0].toLowerCase() + name.slice(1),
@@ -131,6 +135,7 @@ export class AbstractViewSettings
rl.app.Remote.saveSetting(name, value,
iError => {
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
+// iError || Settings.set(name, value);
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
}
);
@@ -138,6 +143,10 @@ export class AbstractViewSettings
});
}
+ /**
+ * Foreach name if this[name] does not exists, create as observable with value of SettingsGet(name)
+ * Subscribe to this[name], for saving the setting
+ */
addSettings(names)
{
names.forEach(name => {
diff --git a/dev/Knoin/Knoin.js b/dev/Knoin/Knoin.js
index 2dfff9917..12d07d1cb 100644
--- a/dev/Knoin/Knoin.js
+++ b/dev/Knoin/Knoin.js
@@ -74,13 +74,17 @@ const
// vmDom.addEventListener('close', () => vm.modalVisible(false));
// show/hide popup/modal
+ // transitionend is called for each property, so we only listen to `opacity`
+ // as defined in CSS by `dialog:not(.animate)`
const endShowHide = e => {
- if (e.target === vmDom) {
+ if (e.target === vmDom && 'opacity' === e.propertyName) {
if (vmDom.classList.contains('animate')) {
vm.afterShow?.();
+ fireEvent('rl-vm-visible', vm);
} else {
vmDom.close();
vm.afterHide?.();
+// fireEvent('rl-vm-hidden', vm);
}
}
};
@@ -163,7 +167,7 @@ const
if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) {
// Close all popups
for (let vm of visiblePopups) {
- (false === vm.onClose()) || vm.close();
+ vm.tryToClose();
}
let vmScreen = screen(screenName);
diff --git a/dev/Mime/Parser.js b/dev/Mime/Parser.js
index 2816d07a3..eb13c199b 100644
--- a/dev/Mime/Parser.js
+++ b/dev/Mime/Parser.js
@@ -83,7 +83,7 @@ export function ParseMime(text)
get body() {
let body = this.bodyRaw,
charset = this.header('content-type')?.params.charset,
- encoding = this.headerValue('content-transfer-encoding');
+ encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
if ('quoted-printable' == encoding) {
body = QPDecode(body);
} else if ('base64' == encoding) {
@@ -94,7 +94,7 @@ export function ParseMime(text)
get dataUrl() {
let body = this.bodyRaw,
- encoding = this.headerValue('content-transfer-encoding');
+ encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
if ('base64' == encoding) {
body = body.replace(/\r?\n/g, '');
} else {
@@ -112,7 +112,7 @@ export function ParseMime(text)
}
getByContentType(type) {
- if (type == this.headerValue('content-type')) {
+ if (type == this.headerValue('content-type')?.toLowerCase()) {
return this;
}
let i = 0, p = this.parts, part;
diff --git a/dev/Mime/Utils.js b/dev/Mime/Utils.js
index 014660cf3..4927898f3 100644
--- a/dev/Mime/Utils.js
+++ b/dev/Mime/Utils.js
@@ -18,6 +18,7 @@ export function MimeToMessage(data, message)
subject = struct.headerValue('subject');
html = html ? html.body : '';
+ // Content-Type: ...; protected-headers="v1"
subject && message.subject(subject);
// EmailCollectionModel
@@ -99,6 +100,4 @@ export function MimeToMessage(data, message)
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
message.pgpSigned(true);
}
-
- // TODO: Verify instantly?
}
diff --git a/dev/Model/AttachmentCollection.js b/dev/Model/AttachmentCollection.js
index 4715d219d..016110efe 100644
--- a/dev/Model/AttachmentCollection.js
+++ b/dev/Model/AttachmentCollection.js
@@ -1,3 +1,4 @@
+import { baseCollator } from 'Common/Translator';
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { AttachmentModel } from 'Model/Attachment';
@@ -11,7 +12,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/
static reviveFromJson(items) {
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
- let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
+ let collator = baseCollator(true);
attachments.sort((a, b) => {
if (a.isInline()) {
if (!b.isInline()) {
diff --git a/dev/Model/Contact.js b/dev/Model/Contact.js
index 5c254b9c7..3ff94dc35 100644
--- a/dev/Model/Contact.js
+++ b/dev/Model/Contact.js
@@ -245,7 +245,7 @@ export class ContactModel extends AbstractModel {
this.namePrefix(),
this.nameSuffix()
]/*, params, group*/);
-// jCard.parseFullName({set:true});
+ jCard.parseFullName({set:true});
['nickname', 'note', 'title'].forEach(field =>
this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field)
diff --git a/dev/Model/Email.js b/dev/Model/Email.js
index 077aab37b..b4a77efcf 100644
--- a/dev/Model/Email.js
+++ b/dev/Model/Email.js
@@ -73,4 +73,9 @@ export class EmailModel extends AbstractModel {
}
return result || name;
}
+
+ get domain() {
+ return this.email.replace(/^.+@([^@]+)$/, '$1');
+// return this.email.slice(this.email.lastIndexOf('@'))
+ }
}
diff --git a/dev/Model/FolderCollection.js b/dev/Model/FolderCollection.js
index df2cdcdb9..228b4c502 100644
--- a/dev/Model/FolderCollection.js
+++ b/dev/Model/FolderCollection.js
@@ -29,6 +29,8 @@ import { FileInfo } from 'Common/File';
import { FolderPopupView } from 'View/Popup/Folder';
import { showScreenPopup } from 'Knoin/Knoin';
+import { isAllowedKeyword } from 'Stores/User/Folder';
+
const
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
@@ -102,7 +104,7 @@ export const
// Repeat every 15 minutes?
// this.foldersTimeout = setTimeout(loadFolders, 900000);
})
- .catch(() => fCallback && setTimeout(fCallback, 1, false));
+ .catch(e => fCallback && setTimeout(fCallback, 1, false, e));
};
export class FolderCollectionModel extends AbstractCollectionModel
@@ -411,6 +413,8 @@ export class FolderModel extends AbstractModel {
canBeSubscribed: () => this.selectable()
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
+ optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
+
/**
* Folder is visible when:
* - hasVisibleSubfolders()
@@ -467,16 +471,25 @@ export class FolderModel extends AbstractModel {
detailedName: () => this.name() + ' ' + this.nameInfo(),
- hasSubscribedUnreadMessagesSubfolders: () =>
- !!this.subFolders().find(
- folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders()
- )
-/*
- !!this.subFolders().filter(
- folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders()
- ).length
-*/
- ,href: () => this.canBeSelected() && mailBox(this.fullNameHash)
+ icon: () => {
+ switch (this.type())
+ {
+ case 1: return '📥'; // FolderType.Inbox
+ case 2: return '📧'; // FolderType.Sent icon-paper-plane
+ case 3: return '🗎'; // FolderType.Drafts
+ case 4: return '⚠'; // FolderType.Junk
+ case 5: return '🗑'; // FolderType.Trash
+ case 6: return '🗄'; // FolderType.Archive
+ }
+ return null;
+ },
+
+ hasUnreadInSub: () =>
+ this.subFolders().some(
+ folder => folder.unreadEmails() | folder.hasUnreadInSub()
+ ),
+
+ href: () => this.canBeSelected() && mailBox(this.fullNameHash)
});
}
diff --git a/dev/Model/Identity.js b/dev/Model/Identity.js
index 89beda875..4a93b2d81 100644
--- a/dev/Model/Identity.js
+++ b/dev/Model/Identity.js
@@ -1,19 +1,16 @@
-import { AbstractModel } from 'Knoin/AbstractModel';
+//import { AbstractModel } from 'Knoin/AbstractModel';
+import { EmailModel } from 'Model/Email';
import { addObservablesTo, addComputablesTo } from 'External/ko';
-export class IdentityModel extends AbstractModel {
- /**
- * @param {string} id
- * @param {string} email
- */
+export class IdentityModel extends EmailModel /*AbstractModel*/ {
constructor() {
super();
addObservablesTo(this, {
id: '',
label: '',
- email: '',
- name: '',
+// email: '',
+// name: '',
replyTo: '',
bcc: '',
@@ -27,23 +24,28 @@ export class IdentityModel extends AbstractModel {
smimeKey: '',
smimeCertificate: '',
+ smimeCertificateChain: '',
- askDelete: false
+ askDelete: false,
+
+ exists: false
});
addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
- smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()),
- smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
+ smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
+ smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()),
+ smimeCertificateChainValid: () => !this.smimeCertificateChain()
+ || /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificateChain())
});
}
/**
* @returns {string}
*/
- formattedName() {
- const name = this.name(),
- email = this.email(),
+ toString() {
+ const name = this.name,
+ email = this.email,
label = this.label();
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
}
diff --git a/dev/Model/Message.js b/dev/Model/Message.js
index ae2f3687e..9b320c14e 100644
--- a/dev/Model/Message.js
+++ b/dev/Model/Message.js
@@ -8,7 +8,7 @@ import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { serverRequestRaw, proxy } from 'Common/Links';
import { addObservablesTo, addComputablesTo } from 'External/ko';
-import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
+import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo, RFC822 } from 'Common/File';
@@ -18,15 +18,73 @@ import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection';
//import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { AbstractModel } from 'Knoin/AbstractModel';
-import PreviewHTML from 'Html/PreviewMessage.html';
-
import { LanguageStore } from 'Stores/Language';
import Remote from 'Remote/User/Fetch';
import { MimeToMessage } from 'Mime/Utils';
+import { PgpUserStore } from 'Stores/User/Pgp';
+import { IdentityUserStore } from 'Stores/User/Identity';
+import { Passphrases } from 'Storage/Passphrases';
+
const
+ PreviewHTML = `
+
+
+
+
+
+
+`,
+
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
toggleTag = (message, keyword) => {
@@ -81,7 +139,6 @@ export class MessageModel extends AbstractModel {
// autocrypt: ko.observableArray(),
hasVirus: null, // or boolean when scanned
priority: 3, // Normal
- internalTimestamp: 0,
senderEmailsString: '',
senderClearEmailsString: '',
isSpam: false,
@@ -104,8 +161,7 @@ export class MessageModel extends AbstractModel {
plain: '',
html: '',
dateTimestamp: 0,
-
- deleted: false,
+ dateTimestampSource: 0,
// Also used by Selector
focused: false,
@@ -152,22 +208,21 @@ export class MessageModel extends AbstractModel {
isUnseen: () => !this.flags().includes('\\seen'),
isFlagged: () => this.flags().includes('\\flagged'),
+ isDeleted: () => this.flags().includes('\\deleted'),
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing'),
tagOptions: () => {
const tagOptions = [];
- FolderUserStore.currentFolder().permanentFlags.forEach(value => {
- if (isAllowedKeyword(value)) {
- let lower = value.toLowerCase();
- tagOptions.push({
- css: 'msgflag-' + lower,
- value: value,
- checked: this.flags().includes(lower),
- label: i18n('MESSAGE_TAGS/'+lower, 0, value),
- toggle: (/*obj*/) => toggleTag(this, value)
- });
- }
+ FolderUserStore.currentFolder().optionalTags().forEach(value => {
+ let lower = value.toLowerCase();
+ tagOptions.push({
+ css: 'msgflag-' + lower,
+ value: value,
+ checked: this.flags().includes(lower),
+ label: i18n('MESSAGE_TAGS/'+lower, 0, value),
+ toggle: (/*obj*/) => toggleTag(this, value)
+ });
});
return tagOptions
},
@@ -311,7 +366,6 @@ export class MessageModel extends AbstractModel {
lineAsCss(flags=1) {
let classes = [];
forEachObjectEntry({
- deleted: this.deleted(),
selected: this.selected(),
checked: this.checked(),
unseen: this.isUnseen(),
@@ -513,7 +567,7 @@ export class MessageModel extends AbstractModel {
hasImages = true;
},
attr = 'data-x-src',
- src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
+ src, useProxy = !!SettingsGet('proxyExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => {
src = node.getAttribute(attr);
if (isValid(src)) {
@@ -548,4 +602,140 @@ export class MessageModel extends AbstractModel {
return result.html || plainToHtml(this.plain());
}
+ async decrypt() {
+ const msg = this;
+ if (msg.pgpEncrypted() && !msg.pgpDecrypted()) {
+ return await msg.pgpDecrypt().then(()=>msg.pgpDecrypted());
+ }
+ if (msg.smimeEncrypted() && !msg.smimeDecrypted()) {
+ return await msg.smimeDecrypt().then(()=>msg.smimeDecrypted());
+ }
+ return true;
+ }
+/*
+ async decrypted() {
+ return msg.pgpDecrypted() || msg.smimeDecrypted());
+ }
+*/
+ async pgpDecrypt() {
+ const oMessage = this,
+ data = oMessage.pgpEncrypted();
+ delete data.error;
+ await PgpUserStore.decrypt(oMessage).then(result => {
+ if (!result) {
+ // TODO: translate
+ throw Error('Decryption failed, canceled or not possible');
+ }
+ oMessage.pgpDecrypted(true);
+ if (result.data) {
+ MimeToMessage(result.data, oMessage);
+ oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
+ if (result.signatures?.length) {
+ oMessage.pgpSigned({
+ signatures: result.signatures,
+ success: !!result.signatures.length
+ });
+ }
+ }
+ })
+ .catch(e => {
+ data.error = e.message;
+ })
+ .finally(() => {
+ oMessage.pgpEncrypted(data);
+ });
+ }
+
+ pgpVerify(/*self, event*/) {
+ const oMessage = this;
+ PgpUserStore.verify(oMessage).then(result => {
+ if (result) {
+ oMessage.pgpSigned(result);
+ } else {
+ alert('Verification failed or no valid public key found');
+ }
+/*
+ if (result?.success) {
+ i18n('CRYPTO/GOOD_SIGNATURE', {
+ USER: validKey.user + ' (' + validKey.id + ')'
+ });
+ message.getText()
+ } else {
+ const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
+ additional = keyIds
+ ? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
+ : '';
+
+ i18n('CRYPTO/ERROR', {
+ TYPE: 'OpenPGP',
+ ERROR: 'message'
+ }) + (additional ? ' (' + additional + ')' : '');
+ }
+*/
+ });
+ }
+
+ async smimeDecrypt() {
+ const message = this;
+ const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
+ identity = IdentityUserStore.find(item => addresses.includes(item.email)),
+ data = message.smimeEncrypted(); // { partId: "1" }
+ if (data && identity) {
+ delete data.error;
+ let pass, params = { ...data }; // clone
+ params.folder = message.folder;
+ params.uid = message.uid;
+// params.bodyPart = params.bodyPart?.raw;
+ params.certificate = identity.smimeCertificate();
+ params.privateKey = identity.smimeKey();
+ if (identity.smimeKeyEncrypted()) {
+ pass = await Passphrases.ask(identity,
+ i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
+ 'CRYPTO/DECRYPT'
+ );
+ if (!pass) {
+ return;
+ }
+ params.passphrase = pass?.password;
+ }
+ await Remote.post('SMimeDecryptMessage', null, params).then(response => {
+ if (response?.Result?.data) {
+ message.smimeDecrypted(true);
+ MimeToMessage(response.Result.data, message);
+ message.html() ? message.viewHtml() : message.viewPlain();
+ pass && pass.remember && Passphrases.handle(identity, pass.password);
+ if ('signed' in response.Result) {
+ message.smimeSigned(response.Result.signed);
+ }
+ }
+ }).catch(e => {
+ data.error = e.message
+ })
+ .finally(() => {
+ message.smimeEncrypted(data);
+ });
+ }
+ }
+
+ smimeVerify(/*self, event*/) {
+ const message = this,
+ data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
+ if (data) {
+ const params = { ...data }; // clone
+ params.folder = message.folder;
+ params.uid = message.uid;
+ params.bodyPart = data.bodyPart?.raw;
+ params.sigPart = data.sigPart?.bodyRaw;
+ Remote.post('SMimeVerifyMessage', null, params).then(response => {
+ if (response?.Result) {
+ if (response.Result.body) {
+ MimeToMessage(response.Result.body, message);
+ message.html() ? message.viewHtml() : message.viewPlain();
+ }
+ data.success = response.Result.success;
+ message.smimeSigned(data);
+ }
+ });
+ }
+ }
}
diff --git a/dev/Model/MessageCollection.js b/dev/Model/MessageCollection.js
index 034c64aee..93ec76cbb 100644
--- a/dev/Model/MessageCollection.js
+++ b/dev/Model/MessageCollection.js
@@ -30,12 +30,13 @@ export class MessageCollectionModel extends AbstractCollectionModel
let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => {
// If message is currently viewed, use that.
- // Maybe then use msg.revivePropertiesFromJson(message) ?
- message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
- if (message) {
- message.deleted(false);
- return message;
+ if (msg && msg.hash === message.hash) {
+ msg.revivePropertiesFromJson(message);
+ message = msg;
+ } else {
+ message = MessageModel.reviveFromJson(message);
}
+ return message;
});
}
}
diff --git a/dev/Model/MimeHeaderAutocrypt.js b/dev/Model/MimeHeaderAutocrypt.js
index b1e65271a..cfd12ea57 100644
--- a/dev/Model/MimeHeaderAutocrypt.js
+++ b/dev/Model/MimeHeaderAutocrypt.js
@@ -10,18 +10,20 @@ export class MimeHeaderAutocryptModel/* extends AbstractModel*/
if (value) {
value.split(';').forEach(entry => {
- entry = entry.split('=');
+ entry = entry.match(/^([^=]+)=(.*)$/);
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, '');
- this[trim(entry[0]).replace('-', '_')] = trim(entry[1]);
+ this[trim(entry[1]).replace('-', '_')] = trim(entry[2]);
});
+ this.keydata = this.keydata.replace(/\s+/g, '\n');
}
}
toString() {
+ let result = `addr=${this.addr}; `;
if ('mutual' === this.prefer_encrypt) {
- return `addr=${this.addr}; prefer-encrypt=mutual; keydata=${this.keydata}`;
+ result += 'prefer-encrypt=mutual; ';
}
- return `addr=${this.addr}; keydata=${this.keydata}`;
+ return result + 'keydata=' + this.keydata.replace(/\n/g, '\n ');
}
pem() {
diff --git a/dev/Remote/AbstractFetch.js b/dev/Remote/AbstractFetch.js
index 0c2aad30e..f05af9915 100644
--- a/dev/Remote/AbstractFetch.js
+++ b/dev/Remote/AbstractFetch.js
@@ -8,11 +8,11 @@ let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => {
- const err = data ? data.ErrorCode : null;
+ const err = data ? data.code : null;
if (Notifications.InvalidToken === err) {
- console.error(getNotification(err));
+ console.error(getNotification(err) + ` (${data.messageAdditional})`);
// alert(getNotification(err));
- rl.logoutReload();
+ setTimeout(rl.logoutReload, 5000);
} else if ([
Notifications.AuthError,
Notifications.ConnectionError,
@@ -131,7 +131,7 @@ export class AbstractFetchRemote
fetchJSON(sAction, getURL(sGetAdd),
sGetAdd ? null : (params || {}),
- undefined === iTimeout ? 30000 : pInt(iTimeout),
+ pInt(iTimeout ?? 30000),
async data => {
let iError = 0;
if (data) {
@@ -144,7 +144,7 @@ export class AbstractFetchRemote
iJsonErrorCount = 0;
} else {
checkResponseError(data);
- iError = data.ErrorCode || Notifications.UnknownError
+ iError = data.code || Notifications.UnknownError
}
}
@@ -196,7 +196,7 @@ export class AbstractFetchRemote
return Promise.reject(new FetchError(Notifications.JsonParse));
}
- if (111 === data?.ErrorCode && rl.app.ask && await rl.app.ask.cryptkey()) {
+ if (111 === data?.code && rl.app.ask && await rl.app.ask.cryptkey()) {
return this.post(action, fTrigger, params, timeOut);
}
/*
@@ -222,8 +222,8 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) {
checkResponseError(data);
return Promise.reject(new FetchError(
- data ? data.ErrorCode : 0,
- data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
+ data ? data.code : 0,
+ data ? (data.messageAdditional || data.message) : ''
));
}
diff --git a/dev/Screen/AbstractSettings.js b/dev/Screen/AbstractSettings.js
index 8d19aeb67..78813c423 100644
--- a/dev/Screen/AbstractSettings.js
+++ b/dev/Screen/AbstractSettings.js
@@ -117,7 +117,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
rules = {
subname: /^(.*)$/,
normalize_: (rquest, vals) => {
- vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
+ vals.subname = pString(vals.subname ?? defaultRoute);
return [vals.subname];
}
};
diff --git a/dev/Screen/User/MailBox.js b/dev/Screen/User/MailBox.js
index 6b1e605ef..d2831a63a 100644
--- a/dev/Screen/User/MailBox.js
+++ b/dev/Screen/User/MailBox.js
@@ -109,7 +109,7 @@ export class MailBoxUserScreen extends AbstractScreen {
*/
onBuild() {
doc.addEventListener('click', event =>
- event.target.closest('#rl-right') && moveAction(false)
+ event.target.closest('#rl-right') && moveAction(0)
);
}
diff --git a/dev/Settings/Admin/About.js b/dev/Settings/Admin/About.js
index 52208119f..456fa250d 100644
--- a/dev/Settings/Admin/About.js
+++ b/dev/Settings/Admin/About.js
@@ -16,6 +16,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
coreWarning: false,
coreVersion: '',
coreVersionCompare: -2,
+ php64: true,
load1: 0,
load5: 0,
load15: 0,
@@ -69,6 +70,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
this.coreWarning(!!data.core.warning);
this.coreVersion(data.core.version || '');
this.coreVersionCompare(data.core.versionCompare);
+ this.php64(data.php[1].loaded);
} else {
this.coreReal(false);
this.coreWarning(false);
@@ -78,6 +80,10 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
});
}
+ clearCache() {
+ Remote.request('AdminClearCache');
+ }
+
updateCoreData() {
if (!this.coreUpdating()) {
this.coreUpdating(true);
diff --git a/dev/Settings/Admin/Config.js b/dev/Settings/Admin/Config.js
index 9aa954e1f..5c33b13f2 100644
--- a/dev/Settings/Admin/Config.js
+++ b/dev/Settings/Admin/Config.js
@@ -10,7 +10,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
constructor() {
this.config = ko.observableArray();
- this.search = ko.observableArray();
+ this.search = ko.observable('');
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
this.search.subscribe(value => {
diff --git a/dev/Settings/Admin/Domains.js b/dev/Settings/Admin/Domains.js
index 80fc9bff4..1a769bb14 100644
--- a/dev/Settings/Admin/Domains.js
+++ b/dev/Settings/Admin/Domains.js
@@ -18,8 +18,8 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
testUsername() {
Remote.request('AdminDomainMatch',
(iError, oData) => {
- if (oData?.Result?.domain) {
- alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`);
+ if (oData?.Result?.name) {
+ alert(`${oData.Result.email} matched domain: ${oData.Result.name}`);
} else {
alert('No domain match');
}
diff --git a/dev/Settings/Admin/Packages.js b/dev/Settings/Admin/Packages.js
index 76286d5e6..37fd18ee6 100644
--- a/dev/Settings/Admin/Packages.js
+++ b/dev/Settings/Admin/Packages.js
@@ -18,7 +18,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
this.addSettings(['pluginsEnable']);
addObservablesTo(this, {
- packagesError: ''
+ packagesError: '',
+ search: ''
});
this.packages = PackageAdminStore;
@@ -30,6 +31,19 @@ export class AdminSettingsPackages extends AbstractViewSettings {
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
});
+
+ this.search.subscribe(value => {
+ const v = value.toLowerCase(),
+ qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
+ match = node => node.textContent.toLowerCase().includes(v);
+ if (v.length) {
+ qsa(this.viewModelDom, 'td:first-child', td => {
+ td.parentNode.hidden = !match(td);
+ });
+ } else {
+ qsa(this.viewModelDom, 'tr[hidden]', n => n.hidden = false);
+ }
+ });
}
onShow() {
@@ -68,7 +82,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) {
this.packagesError(
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
- + (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
+ + (data.message ? ':\n' + data.message : '')
);
} else if (data.Result.Reload) {
location.reload();
@@ -113,8 +127,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) {
plugin.enabled(disable);
this.packagesError(
- (Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
- ? data.ErrorMessage
+ (Notifications.UnsupportedPluginPackage === iError && data?.message)
+ ? data.message
: getNotification(iError)
);
}
diff --git a/dev/Settings/Admin/Security.js b/dev/Settings/Admin/Security.js
index 862fe4a0f..c9e9d8276 100644
--- a/dev/Settings/Admin/Security.js
+++ b/dev/Settings/Admin/Security.js
@@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
- this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
+ this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);
this.weakPassword = rl.app.weakPassword;
@@ -32,6 +32,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
capaOpenPGP: SettingsCapa('OpenPGP')
});
+ this.gnuPGversion = 'GnuPG v' + SettingsGet('gnupg');
+
const reset = () => {
this.saveError(false);
this.saveSuccess(false);
diff --git a/dev/Settings/User/Accounts.js b/dev/Settings/User/Accounts.js
index edaf08ad8..6eec1e148 100644
--- a/dev/Settings/User/Accounts.js
+++ b/dev/Settings/User/Accounts.js
@@ -2,7 +2,7 @@ import ko from 'ko';
//import { koComputable } from 'External/ko';
import { SettingsCapa, SettingsGet } from 'Common/Globals';
-import { loadAccountsAndIdentities } from 'Common/UtilsUser';
+import { loadAccountsAndIdentities, editIdentity } from 'Common/UtilsUser';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
@@ -12,7 +12,6 @@ import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account';
-import { IdentityPopupView } from 'View/Popup/Identity';
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() {
@@ -44,11 +43,11 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
}
addNewIdentity() {
- showScreenPopup(IdentityPopupView);
+ editIdentity();
}
editIdentity(identity) {
- showScreenPopup(IdentityPopupView, [identity]);
+ editIdentity(identity);
}
/**
@@ -89,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
accountsAndIdentitiesAfterMove() {
Remote.request('AccountsAndIdentitiesSortOrder', null, {
- Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
+ Accounts: AccountUserStore.filter(item => item.isAdditional()).map(item => item.email),
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
});
}
diff --git a/dev/Settings/User/Contacts.js b/dev/Settings/User/Contacts.js
index b3815ed56..8d21cdb83 100644
--- a/dev/Settings/User/Contacts.js
+++ b/dev/Settings/User/Contacts.js
@@ -2,7 +2,7 @@ import ko from 'ko';
import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals';
-import { i18n, translateTrigger } from 'Common/Translator';
+import { i18n, translateTrigger, getErrorMessage } from 'Common/Translator';
import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch';
@@ -15,6 +15,7 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
this.syncUrl = ContactUserStore.syncUrl;
this.syncUser = ContactUserStore.syncUser;
this.syncPass = ContactUserStore.syncPass;
+ this.syncError = ko.observable('');
this.syncModeOptions = koComputable(() => {
translateTrigger();
@@ -48,4 +49,15 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
})
);
}
+
+ test() {
+ this.syncError('');
+ Remote.request('TestContactsSyncData', (iError, data) => {
+ iError && this.syncError(data.messageAdditional || data.message || getErrorMessage(iError, data));
+ }, {
+ Url: ContactUserStore.syncUrl(),
+ User: ContactUserStore.syncUser(),
+ Password: ContactUserStore.syncPass()
+ })
+ }
}
diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js
index cb1cdc73b..d0f57062c 100644
--- a/dev/Settings/User/General.js
+++ b/dev/Settings/User/General.js
@@ -6,15 +6,16 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { setRefreshFoldersInterval } from 'Common/Folders';
import { Settings, SettingsGet } from 'Common/Globals';
import { WYSIWYGS } from 'Common/HtmlEditor';
-import { isArray } from 'Common/Utils';
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
+import { editIdentity } from 'Common/UtilsUser';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin';
import { AppUserStore } from 'Stores/User/App';
import { LanguageStore } from 'Stores/Language';
+import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification';
@@ -22,13 +23,14 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
-import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages';
export class UserSettingsGeneral extends AbstractViewSettings {
constructor() {
super();
+ this.mailto = ko.observable(!!navigator.registerProtocolHandler);
+
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
this.hourCycle = LanguageStore.hourCycle;
@@ -37,21 +39,74 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
+ this.minRefreshInterval = SettingsGet('minRefreshInterval');
+
this.desktopNotifications = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
- this.threadsAllowed = AppUserStore.threadsAllowed;
+ const i18nSort = s => i18n('MESSAGE_LIST/SORT_'+s);
+ this.sortSupported = FolderUserStore.hasCapability('SORT');
+ this.sortOptions = [
+ {
+ value: '',
+ text: i18nSort('DATE_DESC')
+ },
+ {
+ value: 'DATE',
+ text: i18nSort('DATE_ASC')
+ },
+ {
+ value: 'FROM',
+ text: i18nSort('FROM_ASC')
+ },
+ {
+ value: 'REVERSE FROM',
+ text: i18nSort('FROM_DESC')
+ },
+ {
+ value: 'REVERSE SIZE',
+ text: i18nSort('SIZE_DESC')
+ },
+ {
+ value: 'SIZE',
+ text: i18nSort('SIZE_ASC')
+ },
+ {
+ value: 'SUBJECT',
+ text: i18nSort('SUBJECT_ASC')
+ },
+ {
+ value: 'REVERSE SUBJECT',
+ text: i18nSort('SUBJECT_DESC')
+ },
+ {
+ value: 'REVERSE ARRIVAL',
+ text: i18nSort('ARRIVAL_DESC')
+ },
+ {
+ value: 'ARRIVAL',
+ text: i18nSort('ARRIVAL_ASC')
+ }
+ ];
- ['useThreads',
+ this.threadsAllowed = AppUserStore.threadsAllowed;
+ // 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
+ this.threadAlgorithms = ko.observableArray();
+ FolderUserStore.capabilities.forEach(capa =>
+ capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
+ );
+ this.threadAlgorithms.sort((a, b) => a.length - b.length);
+
+ ['defaultSort', 'useThreads', 'threadAlgorithm',
// These use addSetting()
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
- 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
+ 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'minFontSize',
// These are in addSettings()
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
- 'messageReadAuto', 'showNextMessage', 'messageNewWindow'
+ 'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown'
].forEach(name => this[name] = SettingsUserStore[name]);
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
@@ -65,14 +120,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
addComputablesTo(this, {
languageFullName: () => convertLangName(this.language()),
- identityMain: () => {
- const list = this.identities();
- return isArray(list) ? list.find(item => item && !item.id()) : null;
- },
-
identityMainDesc: () => {
- const identity = this.identityMain();
- return identity ? identity.formattedName() : '---';
+ const identity = IdentityUserStore.main();
+ return identity ? identity.toString() : '---';
},
editorDefaultTypes: () => {
@@ -111,13 +161,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('CheckMailInterval');
this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel');
+ this.addSetting('minFontSize');
+ this.addSetting('defaultSort');
this.addSettings([
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
- 'messageReadAuto', 'showNextMessage', 'messageNewWindow',
+ 'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown',
'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => {
@@ -146,6 +198,11 @@ export class UserSettingsGeneral extends AbstractViewSettings {
Remote.saveSetting('UseThreads', value);
},
+ threadAlgorithm: value => {
+ MessagelistUserStore([]);
+ Remote.saveSetting('threadAlgorithm', value);
+ },
+
checkMailInterval: () => {
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
}
@@ -153,8 +210,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
}
editMainIdentity() {
- const identity = this.identityMain();
- identity && showScreenPopup(IdentityPopupView, [identity]);
+ editIdentity(IdentityUserStore.main());
}
testSoundNotification() {
@@ -168,4 +224,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
selectLanguage() {
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
}
+
+ registerMailto() {
+ console.log(`mailto = ${location.protocol}//${location.host}${location.pathname}?mailto`);
+ navigator.registerProtocolHandler(
+ 'mailto',
+ `${location.protocol}//${location.host}${location.pathname}?mailto&to=%s`,
+ (SettingsGet('title') || 'SnappyMail')
+ );
+ alert(i18n('GLOBAL/DONE'));
+ this.mailto(0);
+ }
}
diff --git a/dev/Settings/User/Security.js b/dev/Settings/User/Security.js
index bbb8e9dd5..337f65240 100644
--- a/dev/Settings/User/Security.js
+++ b/dev/Settings/User/Security.js
@@ -18,8 +18,6 @@ import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
import { SMimeUserStore } from 'Stores/User/SMime';
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
-import Remote from 'Remote/User/Fetch';
-
export class UserSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
@@ -28,9 +26,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
this.autoLogoutOptions = koComputable(() => {
translateTrigger();
return [
- { id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') },
+ { id: 0, name: i18n('SETTINGS_SECURITY/NEVER') },
{ id: 5, name: relativeTime(300) },
- { id: 10, name: relativeTime(600) },
+ { id: 15, name: relativeTime(900) },
{ id: 30, name: relativeTime(1800) },
{ id: 60, name: relativeTime(3600) },
{ id: 120, name: relativeTime(7200) },
@@ -40,6 +38,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
});
this.addSetting('AutoLogout');
+ this.keyPassForget = SettingsUserStore.keyPassForget;
+ this.addSetting('keyPassForget');
+
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
@@ -62,9 +63,7 @@ export class UserSettingsSecurity extends AbstractViewSettings {
}
importToOpenPGP() {
- OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
- (iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
- );
+ OpenPGPUserStore.loadBackupKeys();
}
importToSMime() {
diff --git a/dev/Settings/User/Themes.js b/dev/Settings/User/Themes.js
index 041680901..ed4fb33bb 100644
--- a/dev/Settings/User/Themes.js
+++ b/dev/Settings/User/Themes.js
@@ -99,8 +99,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
themeBackground.hash(data?.Result?.hash || '');
if (!themeBackground.name() || !themeBackground.hash()) {
let errorMsg = '';
- if (data.ErrorCode) {
- switch (data.ErrorCode) {
+ if (data.code) {
+ switch (data.code) {
case UploadErrorCode.FileIsTooBig:
errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG');
break;
@@ -111,7 +111,7 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
}
}
- themeBackground.error(errorMsg || data.ErrorMessage || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
+ themeBackground.error(errorMsg || data.message || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
}
});
}
diff --git a/dev/Sieve/Commands.js b/dev/Sieve/Commands.js
index 45c2199c2..e2c59e5fa 100644
--- a/dev/Sieve/Commands.js
+++ b/dev/Sieve/Commands.js
@@ -15,7 +15,6 @@ import {
} from 'Sieve/Commands/Actions';
import {
- ConditionalCommand,
ElsIfCommand,
ElseCommand,
IfCommand,
@@ -49,6 +48,7 @@ import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703';
+import { ValidExtListTest } from 'Sieve/Extensions/rfc6134';
import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609';
export const
@@ -67,7 +67,6 @@ export const
IfCommand,
ElsIfCommand,
ElseCommand,
- ConditionalCommand,
RequireCommand,
StopCommand,
// Action commands
@@ -129,6 +128,8 @@ export const
ReplaceCommand,
EncloseCommand,
ExtractTextCommand,
+ // rfc6134
+ ValidExtListTest,
// rfc6609
IncludeCommand,
ReturnCommand,
@@ -146,6 +147,17 @@ export const
return commands;
},
+ unavailableCommands = () => {
+ let commands = {};
+ AllCommands.forEach(cmd => {
+ const obj = new cmd, requires = obj.require;
+ if (requires && !(Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string))) {
+ commands[obj.identifier] = cmd;
+ }
+ });
+ return commands;
+ },
+
availableActions = () => {
let actions = {}, id;
AllCommands.forEach(cmd => {
diff --git a/dev/Sieve/Commands/Actions.js b/dev/Sieve/Commands/Actions.js
index 3d0665a7d..d38f5bcc3 100644
--- a/dev/Sieve/Commands/Actions.js
+++ b/dev/Sieve/Commands/Actions.js
@@ -21,6 +21,10 @@ export class FileIntoCommand extends ActionCommand
super();
// QuotedString / MultiLine
this._mailbox = new GrammarQuotedString();
+ // https://datatracker.ietf.org/doc/html/rfc3894
+ this.copy = false;
+ // https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
+ this.create = false;
}
get require() { return 'fileinto'; }
@@ -28,9 +32,7 @@ export class FileIntoCommand extends ActionCommand
toString()
{
return 'fileinto'
- // https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
- // https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
+ ((this.create && capa.includes('mailbox')) ? ' :create' : '')
+ ' ' + this._mailbox
+ ';';
@@ -64,15 +66,17 @@ export class RedirectCommand extends ActionCommand
super();
// QuotedString / MultiLine
this._address = new GrammarQuotedString();
+ // https://datatracker.ietf.org/doc/html/rfc3894
+ this.copy = false;
+ // https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
+ this.list = null;
}
toString()
{
return 'redirect'
- // https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
// + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '')
- // https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
+ ' ' + this._address
+ ';';
diff --git a/dev/Sieve/Commands/Controls.js b/dev/Sieve/Commands/Controls.js
index 20144d457..a82ed6f9a 100644
--- a/dev/Sieve/Commands/Controls.js
+++ b/dev/Sieve/Commands/Controls.js
@@ -5,6 +5,7 @@
import {
ControlCommand,
+ GrammarCommands,
GrammarStringList,
GrammarQuotedString
} from 'Sieve/Grammar';
@@ -16,37 +17,55 @@ import {
* elsif
* else
*/
-export class ConditionalCommand extends ControlCommand
+export /*abstract*/ class ConditionalCommand extends ControlCommand
{
- constructor(identifier)
+ constructor()
{
- super(identifier);
- this.test = null;
- }
-
- toString()
- {
- return this.identifier + ' ' + this.test + ' ' + this.commands;
- }
/*
- public function pushArguments(array $args): void
- {
- args.forEach((arg, i) => {
- if (i && ':' === args[i-1][0]) {
- this[args[i-1].replace(':','_')].value = arg.value;
- }
- });
- print_r($args);
- exit;
- }
+ if (this.constructor == ConditionalCommand) {
+ throw Error("Abstract class can't be instantiated.");
+ }
*/
+ super();
+ this.commands = new GrammarCommands;
+ }
}
export class IfCommand extends ConditionalCommand
{
+ constructor()
+ {
+ super();
+ this._test = null; // must be descendent instanceof TestCommand
+ }
+
+ get test()
+ {
+ return this._test;
+ }
+
+ set test(value)
+ {
+/*
+ if (!value instanceof TestCommand) {
+ throw Error("test must be descendent instanceof TestCommand.");
+ }
+*/
+ this._test = value;
+ }
+
+ toString()
+ {
+/*
+ if (!this._test instanceof TestCommand) {
+ throw Error("test must be descendent instanceof TestCommand.");
+ }
+*/
+ return this.identifier + ' ' + this._test + ' ' + this.commands;
+ }
}
-export class ElsIfCommand extends ConditionalCommand
+export class ElsIfCommand extends IfCommand
{
}
diff --git a/dev/Sieve/Commands/Tests.js b/dev/Sieve/Commands/Tests.js
index b93c4db83..093b746a2 100644
--- a/dev/Sieve/Commands/Tests.js
+++ b/dev/Sieve/Commands/Tests.js
@@ -40,8 +40,8 @@ export class AddressTest extends TestCommand
this.header_list = new GrammarStringList;
this.key_list = new GrammarStringList;
// rfc5260#section-6
-// this.index = new GrammarNumber;
-// this.last = false;
+ this.index = new GrammarNumber('');
+ this.last = false;
// rfc5703#section-6
// this.mime
// this.anychild
@@ -67,10 +67,11 @@ export class AddressTest extends TestCommand
}
}
return result
-// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
- + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.address_part
- + ' ' + this.match_type
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.header_list
+ ' ' + this.key_list;
}
@@ -147,9 +148,10 @@ export class EnvelopeTest extends TestCommand
toString()
{
return 'envelope'
- + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.address_part
- + ' ' + this.match_type
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.envelope_part
+ ' ' + this.key_list;
}
@@ -234,8 +236,8 @@ export class HeaderTest extends TestCommand
this.header_names = new GrammarStringList;
this.key_list = new GrammarStringList;
// rfc5260#section-6
-// this.index = new GrammarNumber;
-// this.last = false;
+ this.index = new GrammarNumber('');
+ this.last = false;
// rfc5703#section-6
this.mime = false;
this.anychild = false;
@@ -278,9 +280,10 @@ export class HeaderTest extends TestCommand
}
}
return result
-// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
+ + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.header_names
+ ' ' + this.key_list;
}
@@ -316,11 +319,6 @@ export class NotTest extends TestCommand
{
return 'not ' + this.test;
}
-
- pushArguments()
- {
- throw 'No arguments';
- }
}
/**
diff --git a/dev/Sieve/Extensions/rfc5173.js b/dev/Sieve/Extensions/rfc5173.js
index d538735a4..d72b97f89 100644
--- a/dev/Sieve/Extensions/rfc5173.js
+++ b/dev/Sieve/Extensions/rfc5173.js
@@ -22,8 +22,9 @@ export class BodyTest extends TestCommand
toString()
{
return 'body'
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.body_transform
+ ' ' + this.key_list;
}
diff --git a/dev/Sieve/Extensions/rfc5183.js b/dev/Sieve/Extensions/rfc5183.js
index 7e8ff1388..65e51961b 100644
--- a/dev/Sieve/Extensions/rfc5183.js
+++ b/dev/Sieve/Extensions/rfc5183.js
@@ -13,24 +13,28 @@ export class EnvironmentTest extends TestCommand
constructor()
{
super();
- this.name = new GrammarQuotedString;
+ this._name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
+ get name() { return this._name.value; }
+ set name(v) { this._name.value = v; }
+
get require() { return 'environment'; }
toString()
{
return 'environment'
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
- + ' ' + this.name
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + ' ' + this._name
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
- this.name = args.pop();
+ this._name = args.pop();
}
}
diff --git a/dev/Sieve/Extensions/rfc5229.js b/dev/Sieve/Extensions/rfc5229.js
index bb23ab983..794260185 100644
--- a/dev/Sieve/Extensions/rfc5229.js
+++ b/dev/Sieve/Extensions/rfc5229.js
@@ -21,6 +21,12 @@ export class SetCommand extends ActionCommand
get require() { return 'variables'; }
+ get name() { return this._name.value; }
+ set name(str) { this._name.value = str; }
+
+ get value() { return this._value.value; }
+ set value(str) { this._value.value = str; }
+
toString()
{
return 'set'
@@ -29,12 +35,6 @@ export class SetCommand extends ActionCommand
+ ' ' + this._value;
}
- get name() { return this._name.value; }
- set name(str) { this._name.value = str; }
-
- get value() { return this._value.value; }
- set value(str) { this._value.value = str; }
-
pushArguments(args)
{
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
@@ -57,8 +57,9 @@ export class StringTest extends TestCommand
toString()
{
return 'string'
- + ' ' + this.match_type
- + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.source
+ ' ' + this.key_list;
}
diff --git a/dev/Sieve/Extensions/rfc5230.js b/dev/Sieve/Extensions/rfc5230.js
index 1bc340236..a5937a3bd 100644
--- a/dev/Sieve/Extensions/rfc5230.js
+++ b/dev/Sieve/Extensions/rfc5230.js
@@ -30,33 +30,6 @@ export class VacationCommand extends ActionCommand
// get require() { return ['vacation','vacation-seconds']; }
get require() { return 'vacation'; }
- toString()
- {
- let result = 'vacation';
- if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
- result += ' :seconds ' + this._seconds;
- } else if (0 < this._days.value) {
- result += ' :days ' + this._days;
- }
- if (this._subject.length) {
- result += ' :subject ' + this._subject;
- }
- if (this._from.length) {
- result += ' :from ' + this._from;
-// result += ' :from ' + this.arguments[':from'];
- }
- if (this.addresses.length) {
- result += ' :addresses ' + this.addresses;
- }
- if (this.mime) {
- result += ' :mime';
- }
- if (this._handle.length) {
- result += ' :handle ' + this._handle;
- }
- return result + ' ' + this._reason;
- }
-
get days() { return this._days.value; }
get seconds() { return this._seconds.value; }
get subject() { return this._subject.value; }
@@ -71,6 +44,32 @@ export class VacationCommand extends ActionCommand
set handle(str) { this._handle.value = str; }
set reason(str) { this._reason.value = str; }
+ toString()
+ {
+ let result = 'vacation';
+ if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
+ result += ' :seconds ' + this._seconds;
+ } else if (0 < this._days.value) {
+ result += ' :days ' + this._days;
+ }
+ if (this._subject.length) {
+ result += ' :subject ' + this._subject;
+ }
+ if (this._from.length) {
+ result += ' :from ' + this._from;
+ }
+ if (this.addresses.length) {
+ result += ' :addresses ' + this.addresses;
+ }
+ if (this.mime) {
+ result += ' :mime';
+ }
+ if (this._handle.length) {
+ result += ' :handle ' + this._handle;
+ }
+ return result + ' ' + this._reason;
+ }
+
pushArguments(args)
{
this._reason.value = args.pop().value; // GrammarQuotedString
@@ -81,7 +80,8 @@ export class VacationCommand extends ActionCommand
this.addresses = arg; // GrammarStringList
} else if (i && ':' === args[i-1][0]) {
// :days, :seconds, :subject, :from, :handle
- this[args[i-1].replace(':','_')].value = arg.value;
+ let p = args[i-1].replace(':','_');
+ this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
diff --git a/dev/Sieve/Extensions/rfc5232.js b/dev/Sieve/Extensions/rfc5232.js
index 002ad0b7a..ea56261f5 100644
--- a/dev/Sieve/Extensions/rfc5232.js
+++ b/dev/Sieve/Extensions/rfc5232.js
@@ -43,11 +43,12 @@ class FlagCommand extends ActionCommand
if (args[0] instanceof GrammarQuotedString) {
this._variablename = args[0];
}
- if (args[1] instanceof GrammarString) {
- this.list_of_flags = args[1];
- }
- } else if (args[0] instanceof GrammarString) {
+ args[0] = args[1];
+ }
+ if (args[0] instanceof GrammarStringList) {
this.list_of_flags = args[0];
+ } else if (args[0]) {
+ this.list_of_flags.push(args[0]);
}
}
}
@@ -78,8 +79,9 @@ export class HasFlagTest extends TestCommand
toString()
{
return 'hasflag'
- + ' ' + this.match_type
- + (this.comparator ? ' :comparator ' + this.comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.variable_list
+ ' ' + this.list_of_flags;
}
diff --git a/dev/Sieve/Extensions/rfc5235.js b/dev/Sieve/Extensions/rfc5235.js
index 5561a1a5e..ac1e8406e 100644
--- a/dev/Sieve/Extensions/rfc5235.js
+++ b/dev/Sieve/Extensions/rfc5235.js
@@ -4,7 +4,6 @@
import {
GrammarQuotedString,
- GrammarString,
TestCommand
} from 'Sieve/Grammar';
@@ -14,19 +13,23 @@ export class SpamTestTest extends TestCommand
{
super();
this.percent = false, // 0 - 100 else 0 - 10
- this.value = new GrammarQuotedString;
+ this._value = new GrammarQuotedString;
}
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
- get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
+ get require() { return /:value|:count/.test(this._match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
+
+ get value() { return this._value.value; }
+ set value(v) { this._value.value = v; }
toString()
{
return 'spamtest'
+ (this.percent ? ' :percent' : '')
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
- + ' ' + this.value;
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + ' ' + this._value;
}
pushArguments(args)
@@ -34,8 +37,8 @@ export class SpamTestTest extends TestCommand
args.forEach(arg => {
if (':percent' === arg) {
this.percent = true;
- } else if (arg instanceof GrammarString) {
- this.value = arg;
+ } else if (arg instanceof GrammarQuotedString) {
+ this._value = arg;
}
});
}
@@ -46,24 +49,28 @@ export class VirusTestTest extends TestCommand
constructor()
{
super();
- this.value = new GrammarQuotedString; // 1 - 5
+ this._value = new GrammarQuotedString; // 1 - 5
}
- get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
+ get require() { return ':value' == this._match_type ? ['virustest','relational'] : 'virustest'; }
+
+ get value() { return this._value.value; }
+ set value(v) { this._value.value = v; }
toString()
{
return 'virustest'
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
- + ' ' + this.value;
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + ' ' + this._value;
}
pushArguments(args)
{
args.forEach(arg => {
- if (arg instanceof GrammarString) {
- this.value = arg;
+ if (arg instanceof GrammarQuotedString) {
+ this._value = arg;
}
});
}
diff --git a/dev/Sieve/Extensions/rfc5260.js b/dev/Sieve/Extensions/rfc5260.js
index df1653049..2745a2087 100644
--- a/dev/Sieve/Extensions/rfc5260.js
+++ b/dev/Sieve/Extensions/rfc5260.js
@@ -14,10 +14,10 @@ export class DateTest extends TestCommand
constructor()
{
super();
- this.zone = new GrammarQuotedString;
+ this._zone = new GrammarQuotedString;
this.originalzone = false;
- this.header_name = new GrammarQuotedString;
- this.date_part = new GrammarQuotedString;
+ this._header_name = new GrammarQuotedString;
+ this._date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
// rfc5260#section-6
this.index = new GrammarNumber;
@@ -27,30 +27,40 @@ export class DateTest extends TestCommand
// get require() { return ['date','index']; }
get require() { return 'date'; }
+ get zone() { return this._zone.value; }
+ set zone(v) { this._zone.value = v; }
+
+ get header_name() { return this._header_name.value; }
+ set header_name(v) { this._header_name.value = v; }
+
+ get date_part() { return this._date_part.value; }
+ set date_part(v) { this._date_part.value = v; }
+
toString()
{
return 'date'
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
- + (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
- + ' ' + this.header_name
- + ' ' + this.date_part
+ + (this.originalzone ? ' :originalzone' : (this._zone.length ? ' :zone ' + this._zone : ''))
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + ' ' + this._header_name
+ + ' ' + this._date_part
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
- this.date_part = args.pop();
- this.header_name = args.pop();
+ this._date_part = args.pop();
+ this._header_name = args.pop();
args.forEach((arg, i) => {
if (':originalzone' === arg) {
this.originalzone = true;
} else if (':last' === arg) {
this.last = true;
} else if (i && ':zone' === args[i-1]) {
- this.zone.value = arg.value;
+ this._zone.value = arg.value;
} else if (i && ':index' === args[i-1]) {
this.index.value = arg.value;
}
@@ -63,30 +73,37 @@ export class CurrentDateTest extends TestCommand
constructor()
{
super();
- this.zone = new GrammarQuotedString;
- this.date_part = new GrammarQuotedString;
+ this._zone = new GrammarQuotedString;
+ this._date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'date'; }
+ get zone() { return this._zone.value; }
+ set zone(v) { this._zone.value = v; }
+
+ get date_part() { return this._date_part.value; }
+ set date_part(v) { this._date_part.value = v; }
+
toString()
{
return 'currentdate'
- + (this.zone.length ? ' :zone ' + this.zone : '')
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.match_type
- + ' ' + this.date_part
+ + (this._zone.length ? ' :zone ' + this._zone : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + ' ' + this._date_part
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
- this.date_part = args.pop();
+ this._date_part = args.pop();
args.forEach((arg, i) => {
if (i && ':zone' === args[i-1]) {
- this.zone.value = arg.value;
+ this._zone.value = arg.value;
}
});
}
diff --git a/dev/Sieve/Extensions/rfc5293.js b/dev/Sieve/Extensions/rfc5293.js
index c3067d612..8b9a6a1b8 100644
--- a/dev/Sieve/Extensions/rfc5293.js
+++ b/dev/Sieve/Extensions/rfc5293.js
@@ -16,24 +16,30 @@ export class AddHeaderCommand extends ActionCommand
{
super();
this.last = false;
- this.field_name = new GrammarQuotedString;
- this.value = new GrammarQuotedString;
+ this._field_name = new GrammarQuotedString;
+ this._value = new GrammarQuotedString;
}
get require() { return 'editheader'; }
+ get field_name() { return this._field_name.value; }
+ set field_name(v) { this._field_name.value = v; }
+
+ get value() { return this._value.value; }
+ set value(v) { this._value.value = v; }
+
toString()
{
return this.identifier
+ (this.last ? ' :last' : '')
- + ' ' + this.field_name
- + ' ' + this.value + ';';
+ + ' ' + this._field_name
+ + ' ' + this._value + ';';
}
pushArguments(args)
{
- this.value = args.pop();
- this.field_name = args.pop();
+ this._value = args.pop();
+ this._field_name = args.pop();
this.last = args.includes(':last');
}
}
@@ -47,19 +53,22 @@ export class DeleteHeaderCommand extends ActionCommand
this.last = false;
this.comparator = '',
this.match_type = ':is',
- this.field_name = new GrammarQuotedString;
+ this._field_name = new GrammarQuotedString;
this.value_patterns = new GrammarStringList;
}
get require() { return 'editheader'; }
+ get field_name() { return this._field_name.value; }
+ set field_name(v) { this._field_name.value = v; }
+
toString()
{
return this.identifier
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
- + ' ' + this.field_name
+ + ' ' + this._field_name
+ ' ' + this.value_patterns + ';';
}
@@ -76,10 +85,10 @@ export class DeleteHeaderCommand extends ActionCommand
});
if (l && args[l-1] instanceof GrammarString) {
- this.field_name = args[l-1];
+ this._field_name = args[l-1];
this.value_patterns = args[l];
} else {
- this.field_name = args[l];
+ this._field_name = args[l];
}
}
}
diff --git a/dev/Sieve/Extensions/rfc5429.js b/dev/Sieve/Extensions/rfc5429.js
index 984fa2eb9..d33aead7c 100644
--- a/dev/Sieve/Extensions/rfc5429.js
+++ b/dev/Sieve/Extensions/rfc5429.js
@@ -8,11 +8,11 @@ import {
GrammarString
} from 'Sieve/Grammar';
-class rfc5429Command extends ActionCommand
+class /*abstract*/ rfc5429Command extends ActionCommand
{
- constructor(identifier)
+ constructor()
{
- super(identifier);
+ super();
this._reason = new GrammarQuotedString;
}
@@ -44,7 +44,6 @@ class rfc5429Command extends ActionCommand
*/
export class ErejectCommand extends rfc5429Command
{
- constructor() { super('ereject'); }
get require() { return 'ereject'; }
}
@@ -53,6 +52,5 @@ export class ErejectCommand extends rfc5429Command
*/
export class RejectCommand extends rfc5429Command
{
- constructor() { super('reject'); }
get require() { return 'reject'; }
}
diff --git a/dev/Sieve/Extensions/rfc5435.js b/dev/Sieve/Extensions/rfc5435.js
index a77560ca8..b6f040096 100644
--- a/dev/Sieve/Extensions/rfc5435.js
+++ b/dev/Sieve/Extensions/rfc5435.js
@@ -26,14 +26,16 @@ export class NotifyCommand extends ActionCommand
}
get method() { return this._method.value; }
- get from() { return this._from.value; }
- get importance() { return this._importance.value; }
- get message() { return this._message.value; }
+ set method(str) { this._method.value = str; }
- set method(str) { this._method.value = str; }
- set from(str) { this._from.value = str; }
+ get from() { return this._from.value; }
+ set from(str) { this._from.value = str; }
+
+ get importance() { return this._importance.value; }
set importance(int) { this._importance.value = int; }
- set message(str) { this._message.value = str; }
+
+ get message() { return this._message.value; }
+ set message(str) { this._message.value = str; }
get require() { return 'enotify'; }
@@ -63,7 +65,8 @@ export class NotifyCommand extends ActionCommand
this.options = arg; // GrammarStringList
} else if (i && ':' === args[i-1][0]) {
// :from, :importance, :message
- this[args[i-1].replace(':','_')].value = arg.value;
+ let p = args[i-1].replace(':','_');
+ this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
@@ -99,25 +102,32 @@ export class NotifyMethodCapabilityTest extends TestCommand
constructor()
{
super();
- this.notification_uri = new GrammarQuotedString;
- this.notification_capability = new GrammarQuotedString;
+ this._notification_uri = new GrammarQuotedString;
+ this._notification_capability = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
+ get notification_uri() { return this._notification_uri.value; }
+ set notification_uri(v) { this._notification_uri.value = v; }
+
+ get notification_capability() { return this._notification_capability.value; }
+ set notification_capability(v) { this._notification_capability.value = v; }
+
toString()
{
return 'valid_notify_method '
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + (this.match_type ? ' ' + this.match_type : '')
- + this.notification_uri
- + this.notification_capability
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + this._notification_uri
+ + this._notification_capability
+ this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
- this.notification_capability = args.pop();
- this.notification_uri = args.pop();
+ this._notification_capability = args.pop();
+ this._notification_uri = args.pop();
}
}
diff --git a/dev/Sieve/Extensions/rfc5463.js b/dev/Sieve/Extensions/rfc5463.js
index 68166f275..31d91f47a 100644
--- a/dev/Sieve/Extensions/rfc5463.js
+++ b/dev/Sieve/Extensions/rfc5463.js
@@ -41,18 +41,21 @@ export class ErrorCommand extends ControlCommand
constructor()
{
super();
- this.message = new GrammarQuotedString;
+ this._message = new GrammarQuotedString;
}
get require() { return 'ihave'; }
+ get message() { return this._message.value; }
+ set message(v) { this._message.value = v; }
+
toString()
{
- return 'error ' + this.message + ';';
+ return 'error ' + this._message + ';';
}
pushArguments(args)
{
- this.message = args.pop();
+ this._message = args.pop();
}
}
diff --git a/dev/Sieve/Extensions/rfc5490.js b/dev/Sieve/Extensions/rfc5490.js
index 14a087b1d..94f7605b7 100644
--- a/dev/Sieve/Extensions/rfc5490.js
+++ b/dev/Sieve/Extensions/rfc5490.js
@@ -42,28 +42,35 @@ export class MetadataTest extends TestCommand
constructor()
{
super();
- this.mailbox = new GrammarQuotedString;
- this.annotation_name = new GrammarQuotedString;
+ this._mailbox = new GrammarQuotedString;
+ this._annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'mboxmetadata'; }
+ get mailbox() { return this._mailbox.value; }
+ set mailbox(v) { this._mailbox.value = v; }
+
+ get annotation_name() { return this._annotation_name.value; }
+ set annotation_name(v) { this._annotation_name.value = v; }
+
toString()
{
return 'metadata '
- + ' ' + this.match_type
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.mailbox
- + ' ' + this.annotation_name
+ + ' ' + this._match_type
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + ' ' + this._mailbox
+ + ' ' + this._annotation_name
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
- this.annotation_name = args.pop();
- this.mailbox = args.pop();
+ this._annotation_name = args.pop();
+ this._mailbox = args.pop();
}
}
@@ -75,23 +82,26 @@ export class MetadataExistsTest extends TestCommand
constructor()
{
super();
- this.mailbox = new GrammarQuotedString;
+ this._mailbox = new GrammarQuotedString;
this.annotation_names = new GrammarStringList;
}
get require() { return 'mboxmetadata'; }
+ get mailbox() { return this._mailbox.value; }
+ set mailbox(v) { this._mailbox.value = v; }
+
toString()
{
return 'metadataexists '
- + ' ' + this.mailbox
+ + ' ' + this._mailbox
+ ' ' + this.annotation_names;
}
pushArguments(args)
{
this.annotation_names = args.pop();
- this.mailbox = args.pop();
+ this._mailbox = args.pop();
}
}
@@ -103,25 +113,29 @@ export class ServerMetadataTest extends TestCommand
constructor()
{
super();
- this.annotation_name = new GrammarQuotedString;
+ this._annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'servermetadata'; }
+ get annotation_name() { return this._annotation_name.value; }
+ set annotation_name(v) { this._annotation_name.value = v; }
+
toString()
{
return 'servermetadata '
- + ' ' + this.match_type
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + ' ' + this.annotation_name
+ + ' ' + this._match_type
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + ' ' + this._annotation_name
+ ' ' + this.key_list;
}
pushArguments(args)
{
this.key_list = args.pop();
- this.annotation_name = args.pop();
+ this._annotation_name = args.pop();
}
}
diff --git a/dev/Sieve/Extensions/rfc5703.js b/dev/Sieve/Extensions/rfc5703.js
index c0b5322e8..552e04c2f 100644
--- a/dev/Sieve/Extensions/rfc5703.js
+++ b/dev/Sieve/Extensions/rfc5703.js
@@ -5,6 +5,7 @@
import {
ActionCommand,
ControlCommand,
+ GrammarCommands,
GrammarNumber,
GrammarQuotedString,
GrammarString,
@@ -19,7 +20,8 @@ export class ForEveryPartCommand extends ControlCommand
constructor()
{
super();
- this._name = new GrammarString;
+ this.name = new GrammarString;
+ this.commands = new GrammarCommands;
}
get require() { return 'foreverypart'; }
@@ -27,8 +29,8 @@ export class ForEveryPartCommand extends ControlCommand
toString()
{
let result = 'foreverypart';
- if (this._subject.length) {
- result += ' :name ' + this._name;
+ if (this.name.length) {
+ result += ' :name ' + this.name;
}
return result + ' ' + this.commands;
}
@@ -37,19 +39,22 @@ export class ForEveryPartCommand extends ControlCommand
{
args.forEach((arg, i) => {
if (':name' === arg) {
- this._name.value = args[i+1].value;
+ this.name.value = args[i+1].value;
}
});
}
}
+/**
+ * Must be inside foreverypart
+ */
export class BreakCommand extends ForEveryPartCommand
{
toString()
{
let result = 'break';
- if (this._subject.length) {
- result += ' :name ' + this._name;
+ if (this.name.length) {
+ result += ' :name ' + this.name;
}
return result + ';';
}
@@ -63,14 +68,23 @@ export class ReplaceCommand extends ActionCommand
constructor()
{
super();
- this.mime = false;
- this._subject = new GrammarQuotedString;
- this._from = new GrammarQuotedString;
- this.replacement = new GrammarQuotedString;
+ this.mime = false;
+ this._subject = new GrammarQuotedString;
+ this._from = new GrammarQuotedString;
+ this._replacement = new GrammarQuotedString;
}
get require() { return 'replace'; }
+ get subject() { return this._subject.value; }
+ set subject(str) { this._subject.value = str; }
+
+ get from() { return this._from.value; }
+ set from(str) { this._from.value = str; }
+
+ get replacement() { return this._replacement.value; }
+ set replacement(str) { this._replacement.value = str; }
+
toString()
{
let result = 'replace';
@@ -82,20 +96,20 @@ export class ReplaceCommand extends ActionCommand
}
if (this._from.length) {
result += ' :from ' + this._from;
-// result += ' :from ' + this.arguments[':from'];
}
- return result + this.replacement + ';';
+ return result + this._replacement + ';';
}
pushArguments(args)
{
- this.replacement = args.pop();
+ this._replacement = args.pop();
args.forEach((arg, i) => {
if (':mime' === arg) {
this.mime = true;
} else if (i && ':' === args[i-1][0]) {
// :subject, :from
- this[args[i-1].replace(':','_')].value = arg.value;
+ let p = args[i-1].replace(':','_');
+ this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
@@ -115,6 +129,9 @@ export class EncloseCommand extends ActionCommand
get require() { return 'enclose'; }
+ get subject() { return this._subject.value; }
+ set subject(v) { this._subject.value = v; }
+
toString()
{
let result = 'enclose';
@@ -132,7 +149,8 @@ export class EncloseCommand extends ActionCommand
args.forEach((arg, i) => {
if (i && ':' === args[i-1][0]) {
// :subject, :headers
- this[args[i-1].replace(':','_')].value = arg.value;
+ let p = args[i-1].replace(':','_');
+ this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
}
});
}
@@ -140,6 +158,7 @@ export class EncloseCommand extends ActionCommand
/**
* https://datatracker.ietf.org/doc/html/rfc5703#section-7
+ * Should be inside foreverypart, else empty and flagged as a compilation error
*/
export class ExtractTextCommand extends ActionCommand
{
@@ -148,9 +167,12 @@ export class ExtractTextCommand extends ActionCommand
super();
this.modifiers = [];
this._first = new GrammarNumber;
- this.varname = new GrammarQuotedString;
+ this._varname = new GrammarQuotedString;
}
+ get varname() { return this._varname.value; }
+ set varname(v) { this._varname.value = v; }
+
get require() { return 'extracttext'; }
toString()
@@ -160,12 +182,12 @@ export class ExtractTextCommand extends ActionCommand
if (0 < this._first.value) {
result += ' :first ' + this._first;
}
- return result + ' ' + this.varname + ';';
+ return result + ' ' + this._varname + ';';
}
pushArguments(args)
{
- this.varname = args.pop();
+ this._varname = args.pop();
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
args.includes(modifier) && this.modifiers.push(modifier);
});
diff --git a/dev/Sieve/Extensions/rfc6134.js b/dev/Sieve/Extensions/rfc6134.js
index 872938eab..1bdb7c8b0 100644
--- a/dev/Sieve/Extensions/rfc6134.js
+++ b/dev/Sieve/Extensions/rfc6134.js
@@ -14,7 +14,7 @@ export class ValidExtListTest extends TestCommand
{
constructor()
{
- super();
+ super('valid_ext_list');
this.ext_list_names = new GrammarStringList;
}
diff --git a/dev/Sieve/Extensions/rfc6609.js b/dev/Sieve/Extensions/rfc6609.js
index 4cd5d19ec..8d7fd74ae 100644
--- a/dev/Sieve/Extensions/rfc6609.js
+++ b/dev/Sieve/Extensions/rfc6609.js
@@ -16,18 +16,21 @@ export class IncludeCommand extends ControlCommand
this.global = false; // ':personal' / ':global';
this.once = false;
this.optional = false;
- this.value = new GrammarQuotedString;
+ this._value = new GrammarQuotedString;
}
get require() { return 'include'; }
+ get value() { return this._value.value; }
+ set value(v) { this._value.value = v; }
+
toString()
{
return 'include'
+ (this.global ? ' :global' : '')
+ (this.once ? ' :once' : '')
+ (this.optional ? ' :optional' : '')
- + ' ' + this.value + ';';
+ + ' ' + this._value + ';';
}
pushArguments(args)
@@ -36,7 +39,7 @@ export class IncludeCommand extends ControlCommand
if (':global' === arg || ':once' === arg || ':optional' === arg) {
this[arg.slice(1)] = true;
} else if (arg instanceof GrammarQuotedString) {
- this.value = arg;
+ this._value = arg;
}
});
}
diff --git a/dev/Sieve/Grammar.js b/dev/Sieve/Grammar.js
index 95a142c11..595a34bda 100644
--- a/dev/Sieve/Grammar.js
+++ b/dev/Sieve/Grammar.js
@@ -9,7 +9,7 @@ import {
MULTILINE_DOTSTART
} from 'Sieve/RegEx';
-import { arrayToString, getMatchTypes } from 'Sieve/Utils';
+import { arrayToString, getMatchTypes, getComparators } from 'Sieve/Utils';
/**
* abstract
@@ -18,7 +18,7 @@ export class GrammarString /*extends String*/
{
constructor(value = '')
{
- this._value = value;
+ this._value = value.toString ? value.toString() : value;
}
toString()
@@ -45,41 +45,41 @@ export class GrammarString /*extends String*/
/**
* abstract
*/
-export class GrammarComment extends GrammarString
+export /*abstract*/ class GrammarComment extends GrammarString
{
+/*
+ constructor()
+ {
+ if (this.constructor == GrammarComment) {
+ throw Error("Abstract class can't be instantiated.");
+ }
+ }
+*/
}
/**
* https://tools.ietf.org/html/rfc5228#section-2.9
*/
-export class GrammarCommand
+const cmdNameSuffix = /(test|command|action)$/;
+export /*abstract*/ class GrammarCommand
{
constructor(identifier)
{
- this.identifier = identifier || this.constructor.name.toLowerCase().replace(/(test|command|action)$/, '');
- this.arguments = [];
- this.commands = new GrammarCommands;
+/*
+ if (this.constructor == GrammarCommand) {
+ throw Error("Abstract class can't be instantiated.");
+ }
+*/
+ this.identifier = identifier || this.constructor.name.toLowerCase().replace(cmdNameSuffix, '');
}
toString()
{
let result = this.identifier;
- if (this.arguments.length) {
+ if (this.arguments?.length) {
result += ' ' + arrayToString(this.arguments, ' ');
}
- return result + (
- this.commands.length ? ' ' + this.commands : ';'
- );
- }
-
- getComparators()
- {
- return ['i;ascii-casemap'];
- }
-
- getMatchTypes()
- {
- return [':is', ':contains', ':matches'];
+ return result + ';';
}
pushArguments(args)
@@ -108,78 +108,107 @@ export class GrammarCommands extends Array
/**
* https://tools.ietf.org/html/rfc5228#section-3
*/
-export class ControlCommand extends GrammarCommand
+export /*abstract*/ class ControlCommand extends GrammarCommand
{
+/*
constructor(identifier)
{
- super(identifier);
- this.commands = new GrammarCommands;
- }
-
- toString()
- {
- let result = this.identifier;
- if (this.arguments.length) {
- result += ' ' + arrayToString(this.arguments, ' ');
+ if (this.constructor == ControlCommand) {
+ throw Error("Abstract class can't be instantiated.");
}
- return result + (
- this.commands.length ? ' ' + this.commands : ';'
- );
- }
-
- getComparators()
- {
- return ['i;ascii-casemap'];
- }
-
- getMatchTypes()
- {
- return [':is', ':contains', ':matches'];
+ super(identifier);
}
+*/
}
/**
* https://tools.ietf.org/html/rfc5228#section-4
*/
-export class ActionCommand extends GrammarCommand
+export /*abstract*/ class ActionCommand extends GrammarCommand
{
+/*
constructor(identifier)
{
+ if (this.constructor == ActionCommand) {
+ throw Error("Abstract class can't be instantiated.");
+ }
super(identifier);
}
-
- toString()
- {
- let result = this.identifier;
- if (this.arguments.length) {
- result += ' ' + arrayToString(this.arguments, ' ');
- }
- return result + ';'
- }
+*/
}
/**
* https://tools.ietf.org/html/rfc5228#section-5
*/
-export class TestCommand extends GrammarCommand
+export /*abstract*/ class TestCommand extends GrammarCommand
{
constructor(identifier)
{
+/*
+ if (this.constructor == TestCommand) {
+ throw Error("Abstract class can't be instantiated.");
+ }
+*/
super(identifier);
// Almost every test has a comparator and match_type, so define them here
- this.comparator = '';
- this.match_type = ':is';
+ this._comparator = '';
+ this._match_type = '';
+ this.relational_match = ''; // GrammarQuotedString DQUOTE ( "gt" / "ge" / "lt" / "le" / "eq" / "ne" ) DQUOTE
+ }
+
+ get require() { return /:value|:count/.test(this._match_type) ? 'relational' : ''; }
+
+ get match_type()
+ {
+ return this._match_type;
+ }
+ set match_type(value)
+ {
+ // default?
+ if (':is' == value) {
+ value = '';
+ }
+ if (value.length && !getMatchTypes(0).includes(value)) {
+ throw 'Unsupported match-type ' + value;
+ }
+ if (':list' == value) {
+ this._comparator = '';
+ }
+ if (':count' != value && ':value' != value) {
+ this.relational_match = '';
+ }
+ this._match_type = value;
+ }
+
+ get comparator()
+ {
+ return this._comparator;
+ }
+ set comparator(value)
+ {
+ if (!(value instanceof GrammarQuotedString)) {
+ value = new GrammarQuotedString(value);
+ }
+ // default?
+ if (value.length && 'i;ascii-casemap' != value.value) {
+ if (':list' == this._match_type) {
+ throw 'Comparator not allowed when using :list';
+ }
+ if (!getComparators().includes(value.value)) {
+ throw 'Unsupported comparator ' + value;
+ }
+ this._comparator = value;
+ } else {
+ this._comparator = '';
+ }
}
toString()
{
- // https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
- if (!getMatchTypes().includes(this.match_type)) {
- throw 'Unsupported match-type ' + this.match_type;
- }
return (this.identifier
- + (this.comparator ? ' :comparator ' + this.comparator : '')
- + (this.match_type ? ' ' + this.match_type : '')
+ + (this._comparator ? ' :comparator ' + this._comparator : '')
+ + (this._match_type ? ' ' + this._match_type : '')
+ + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + arrayToString(this.arguments, ' ')).trim();
}
}
@@ -251,6 +280,7 @@ export class GrammarStringList extends Array
{
toString()
{
+ // if there is only a single string, the brackets are optional
if (1 < this.length) {
return '[' + this.join(',') + ']';
}
@@ -259,7 +289,7 @@ export class GrammarStringList extends Array
push(value)
{
- if (!(value instanceof GrammarString)) {
+ if (!(value instanceof GrammarQuotedString)) {
value = new GrammarQuotedString(value);
}
super.push(value);
@@ -286,10 +316,14 @@ GrammarStringList.fromString = list => {
export class GrammarQuotedString extends GrammarString
{
+ constructor(value = '')
+ {
+ super(value instanceof GrammarQuotedString ? value.value : value);
+ }
+
toString()
{
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
-// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
}
}
@@ -324,3 +358,23 @@ GrammarMultiLine.fromString = string => {
}
return new GrammarMultiLine();
}
+
+export class UnknownCommand extends GrammarCommand
+{
+ constructor(identifier)
+ {
+ super(identifier);
+ this.commands = new GrammarCommands;
+ }
+
+ toString()
+ {
+ let result = this.identifier;
+ if (this.arguments?.length) {
+ result += ' ' + arrayToString(this.arguments, ' ');
+ }
+ return result + (
+ this.commands?.length ? ' ' + this.commands : ';'
+ );
+ }
+}
diff --git a/dev/Sieve/Parser.js b/dev/Sieve/Parser.js
index ce929d24e..8515d28b6 100644
--- a/dev/Sieve/Parser.js
+++ b/dev/Sieve/Parser.js
@@ -18,6 +18,7 @@ import {
import {
GrammarBracketComment,
GrammarCommand,
+ GrammarComment,
GrammarHashComment,
GrammarMultiLine,
GrammarNumber,
@@ -27,8 +28,8 @@ import {
GrammarTestList
} from 'Sieve/Grammar';
-import { availableCommands } from 'Sieve/Commands';
-import { ConditionalCommand, RequireCommand } from 'Sieve/Commands/Controls';
+import { availableCommands, unavailableCommands } from 'Sieve/Commands';
+import { ConditionalCommand, IfCommand, RequireCommand } from 'Sieve/Commands/Controls';
import { NotTest } from 'Sieve/Commands/Tests';
const
@@ -92,6 +93,7 @@ export const parseScript = (script, name = 'script.sieve') => {
// Only activate available commands
const Commands = availableCommands();
+ const disabledCommands = unavailableCommands();
let match,
line = 1,
@@ -117,18 +119,20 @@ export const parseScript = (script, name = 'script.sieve') => {
let prev_arg = args[args.length-1];
if (getMatchTypes(0).includes(arg)) {
command.match_type = arg;
- } else if (':value' === prev_arg || ':count' === prev_arg) {
- // Sieve relational [RFC5231] match types
- /^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
- command.match_type = prev_arg + ' ' + arg;
+ } else if (getMatchTypes(0).includes(prev_arg)) {
--args.length;
-// requires.push('relational');
+ if (':value' === prev_arg || ':count' === prev_arg) {
+ // Sieve relational [RFC5231] match types
+ /^"(gt|ge|lt|le|eq|ne)"$/.test(arg) || error('Invalid relational match-type ' + arg);
+ command.relational_match = arg;
+// requires.push('relational');
+ return;
+ }
} else if (':comparator' === prev_arg) {
command.comparator = arg;
--args.length;
- } else {
- args.push(arg);
}
+ args.push(arg);
},
pushArgs = () => {
if (args.length) {
@@ -154,27 +158,30 @@ export const parseScript = (script, name = 'script.sieve') => {
pushArgs();
value = value.toLowerCase();
let new_command;
- if ('if' === value) {
- new_command = new ConditionalCommand(value);
- } else if ('elsif' === value || 'else' === value) {
-// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
- new_command = new ConditionalCommand(value);
- } else if (Commands[value]) {
+ if (Commands[value]) {
+ if ('elsif' === value || 'else' === value) {
+ let valid = false, cmd = (command ? command?.commands : tree), i = cmd?.length;
+ while (i) {
+ cmd[--i];
+ if (cmd[i] instanceof IfCommand) {
+ valid = true;
+ break;
+ } else if (typeof cmd[i] !== 'string' && !(cmd[i] instanceof GrammarComment)) {
+ break;
+ }
+ }
+ valid || error('Not after IF/ELSIF condition');
+ }
if ('allof' === value || 'anyof' === value) {
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
}
new_command = new Commands[value]();
+ } else if (disabledCommands[value]) {
+ console.error('Unsupported command: ' + value);
+ new_command = new disabledCommands[value]();
} else {
- if (command && (
- command instanceof ConditionalCommand
- || command instanceof NotTest
- || command.tests instanceof GrammarTestList)) {
- console.error('Unknown test: ' + value);
- new_command = new TestCommand(value);
- } else {
- console.error('Unknown command: ' + value);
- new_command = new GrammarCommand(value);
- }
+ console.error('Unknown command: ' + value);
+ new_command = new GrammarCommand(value);
}
if (new_command instanceof TestCommand) {
@@ -192,7 +199,7 @@ export const parseScript = (script, name = 'script.sieve') => {
if (command.commands) {
command.commands.push(new_command);
} else {
- error('commands not allowed in "' + command.identifier + '" command');
+ error('command "' + new_command.identifier + '" not allowed in "' + command.identifier + '" command');
}
} else {
tree.push(new_command);
@@ -219,7 +226,8 @@ export const parseScript = (script, name = 'script.sieve') => {
pushArg(GrammarMultiLine.fromString(value));
break;
case T_QUOTED_STRING:
- pushArg(new GrammarQuotedString(value.slice(1,-1)));
+ try { value = JSON.parse(value); } catch(e) { console.error(e, value); }
+ pushArg(new GrammarQuotedString(value));
break;
case T_NUMBER:
pushArg(new GrammarNumber(value));
@@ -268,7 +276,6 @@ export const parseScript = (script, name = 'script.sieve') => {
break;
case T_BLOCK_END:
(command instanceof ConditionalCommand) || error(TokenError[type]);
-// prev_command = command;
command = levels.up();
break;
@@ -293,5 +300,6 @@ export const parseScript = (script, name = 'script.sieve') => {
}
tree.requires = requires;
+ tree.toString = () => tree.join('\r\n');
return tree;
};
diff --git a/dev/Sieve/Utils.js b/dev/Sieve/Utils.js
index 14c58101a..5f5d13f07 100644
--- a/dev/Sieve/Utils.js
+++ b/dev/Sieve/Utils.js
@@ -22,7 +22,7 @@ export const
koComputable = fn => ko.computed(fn, {'pure':true}),
arrayToString = (arr, separator) =>
- arr.map(item => item.toString?.() || item).join(separator),
+ (arr || []).map(item => item.toString?.() || item).join(separator),
/*
getNotificationMessage = code => {
let key = getKeyByValue(Notifications, code);
@@ -58,11 +58,33 @@ export const
serverErrorDesc(text);
},
+ getComparators = (validOnly = 0) => {
+ let result = [
+ // Default
+ 'i;ascii-casemap',
+ ];
+ if (capa.includes('relational') || !validOnly) {
+ result.push('i;octet');
+ }
+ if (capa.includes('comparator-i;ascii-numeric') || !validOnly) {
+ result.push('i;ascii-numeric');
+ }
+ if (capa.includes('comparator-i;unicode-casemap') || !validOnly) {
+ result.push('i;unicode-casemap');
+ }
+ return result;
+ },
+
getMatchTypes = (validOnly = 1) => {
let result = [':is',':contains',':matches'];
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
+ // Only available for tests with a key_list property
if (capa.includes('extlists') || !validOnly) {
result.push(':list');
}
+ if (capa.includes('relational') || !validOnly) {
+ result.push(':value');
+ result.push(':count');
+ }
return result;
};
diff --git a/dev/Sieve/View/Filter.js b/dev/Sieve/View/Filter.js
index 14c6d0b38..41bf85998 100644
--- a/dev/Sieve/View/Filter.js
+++ b/dev/Sieve/View/Filter.js
@@ -30,8 +30,6 @@ const
sDeepPrefix = '\u00A0\u00A0\u00A0',
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
- disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
-
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
@@ -39,7 +37,7 @@ const
id: oItem.fullName,
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
system: false,
- disabled: !oItem.selectable() || disabled == oItem.fullName
+ disabled: !oItem.selectable()
});
}
@@ -68,7 +66,7 @@ export class FilterPopupView extends rl.pluginPopupView {
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
- this.folderSelectList = koComputable(() => folderListOptionsBuilder());
+ this.folderSelectList = koComputable(folderListOptionsBuilder);
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
diff --git a/dev/Sieve/View/Script.js b/dev/Sieve/View/Script.js
index edae504a1..047eae50f 100644
--- a/dev/Sieve/View/Script.js
+++ b/dev/Sieve/View/Script.js
@@ -78,7 +78,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
if (iError) {
self.saveError(true);
- self.errorText(data?.ErrorMessageAdditional || getNotification(iError));
+ self.errorText(data?.messageAdditional || getNotification(iError));
} else {
script.exists() || scripts.push(script);
script.exists(true);
diff --git a/dev/Storage/Client.js b/dev/Storage/Client.js
index f40553a10..0afe98134 100644
--- a/dev/Storage/Client.js
+++ b/dev/Storage/Client.js
@@ -23,7 +23,7 @@ try {
data = data ? decodeURIComponent(data[2]) : null;
data = data ? JSON.parse(data) : {};
win[sName] = {
- getItem: key => data[key] == null ? null : data[key],
+ getItem: key => data[key] ?? null,
setItem: (key, value) => {
data[key] = ''+value; // forces the value to a string
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
diff --git a/dev/Storage/Passphrases.js b/dev/Storage/Passphrases.js
index e9d2ec5e7..0242ddd45 100644
--- a/dev/Storage/Passphrases.js
+++ b/dev/Storage/Passphrases.js
@@ -1,8 +1,155 @@
import { AskPopupView } from 'View/Popup/Ask';
+import { SettingsUserStore } from 'Stores/User/Settings';
+import { SettingsGet } from '../Common/Globals';
+import { isArray } from '../Common/Utils';
export const Passphrases = new WeakMap();
-Passphrases.ask = async (key, sAskDesc, btnText) =>
- Passphrases.has(key)
- ? {password:Passphrases.get(key)/*, remember:false*/}
- : await AskPopupView.password(sAskDesc, btnText);
+Passphrases.ask = async (key, sAskDesc, btnText) => {
+ if (Passphrases.has(key)) {
+ return { password: Passphrases.handle(key)/*, remember:false*/ };
+ } else if (Passphrases.hasInLocalStorage(key)) {
+ return { password: await getFromLocalStorage(key) };
+ } else {
+ const pass = await AskPopupView.password(sAskDesc, btnText,
+ window.crypto.subtle && canUseLocalStorage(key) ? 0b1101 : 0b0101);
+ pass.rememberPermanent && await saveToLocalStorage(key, pass.password);
+ return pass;
+ }
+};
+
+Passphrases._deleteFromSession = Passphrases.delete;
+
+const timeouts = {};
+// get/set accessor to control deletion after N minutes of inactivity
+Passphrases.handle = (key, pass) => {
+ const timeout = SettingsUserStore.keyPassForget();
+ if (timeout && !timeouts[key]) {
+ timeouts[key] = (() => Passphrases._deleteFromSession(key)).debounce(timeout * 60 * 1000);
+ }
+ pass && Passphrases.set(key, pass);
+ timeout && timeouts[key]();
+ return Passphrases.get(key);
+};
+
+const deleteFromLocalStorage = (key) => {
+ const keyId = getKeyId(key);
+ if (keyId) {
+ localStorage.removeItem(keyId);
+ }
+};
+Passphrases.delete = (key) => {
+ deleteFromLocalStorage(key);
+ return Passphrases._deleteFromSession(key);
+};
+
+Passphrases.hasInLocalStorage = (key) => {
+ const keyId = getKeyId(key);
+ return keyId && localStorage.getItem(keyId) !== null;
+};
+
+const saveToLocalStorage = async (key, pass) => {
+ const keyId = getKeyId(key);
+ if (!keyId) {
+ return;
+ }
+
+ if (!pass) {
+ localStorage.removeItem(keyId);
+ return;
+ }
+
+ try {
+ const salt = window.crypto.getRandomValues(new Uint8Array(16));
+ const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), salt);
+
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = await window.crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: iv },
+ derivedKey,
+ new TextEncoder().encode(pass)
+ );
+ localStorage.setItem(keyId, JSON.stringify([
+ btoa(String.fromCharCode.apply(null, salt)),
+ btoa(String.fromCharCode.apply(null, iv)),
+ btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)))
+ ]));
+ } catch (e) {
+ console.error('Passphrases.saveToLocalStorage failed', e);
+ }
+};
+
+const getFromLocalStorage = async (key) => {
+ const keyId = getKeyId(key);
+ if (!keyId) {
+ return undefined;
+ }
+
+ const jsonData = localStorage.getItem(keyId);
+ if (!jsonData) {
+ console.error('Passphrases.getFromLocalStorage failed: no data found');
+ return undefined;
+ }
+
+ try {
+ const saltIvData = JSON.parse(jsonData);
+ if (!saltIvData || !isArray(saltIvData) || saltIvData.length !== 3) {
+ // noinspection ExceptionCaughtLocallyJS
+ throw new Error('invalid passphrase data');
+ }
+ const toUint8 = (str) => new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0)));
+
+ const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), toUint8(saltIvData[0]));
+ const decrypted = await window.crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: toUint8(saltIvData[1])
+ },
+ derivedKey,
+ toUint8(saltIvData[2])
+ );
+ return String.fromCharCode.apply(null, new Uint8Array(decrypted));
+ } catch (e) {
+ localStorage.removeItem(keyId);
+ console.error('Passphrases.getFromLocalStorage failed', e);
+ return undefined;
+ }
+};
+
+const canUseLocalStorage = (key) => getKeyId(key) !== undefined;
+
+const getKeyId = (key) => {
+ if (key && typeof key.id === 'string' && key.id.length > 4 && typeof key.forgetPass === 'function') {
+ // only deal with keys that we can forget (OpenPGB, GnuPG)
+ return key.id + '_local_key';
+ } else {
+ console.info('Passphrases.getKeyId: unsupported key type');
+ return undefined;
+ }
+};
+
+const deriveKeyFromHash = async (hash, salt) => {
+ if (!hash) {
+ throw new Error('empty accountHash');
+ }
+ return window.crypto.subtle.importKey(
+ 'raw',
+ new TextEncoder().encode(hash),
+ { 'name': 'PBKDF2' },
+ false,
+ ['deriveKey']
+ ).then(keyMaterial => {
+ return window.crypto.subtle.deriveKey(
+ {
+ 'name': 'PBKDF2',
+ 'salt': salt,
+ 'iterations': 512,
+ 'hash': 'SHA-256'
+ },
+ keyMaterial,
+ { 'name': 'AES-GCM', 'length': 256 },
+ false,
+ ['encrypt', 'decrypt']
+ );
+ });
+};
diff --git a/dev/Stores/User/Account.js b/dev/Stores/User/Account.js
index b1d45d42f..122ba358e 100644
--- a/dev/Stores/User/Account.js
+++ b/dev/Stores/User/Account.js
@@ -2,8 +2,6 @@ import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
export const AccountUserStore = koArrayWithDestroy();
-AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
-
addObservablesTo(AccountUserStore, {
email: '',
loading: false
diff --git a/dev/Stores/User/Contact.js b/dev/Stores/User/Contact.js
index 89f1b01dd..c95af029a 100644
--- a/dev/Stores/User/Contact.js
+++ b/dev/Stores/User/Contact.js
@@ -39,7 +39,7 @@ ContactUserStore.sync = fResultFunc => {
line = JSON.parse(line);
if ('ContactsSync' === line.Action) {
ContactUserStore.syncing(false);
- fResultFunc?.(line.ErrorCode, line);
+ fResultFunc?.(line.code, line);
}
} catch (e) {
ContactUserStore.syncing(false);
diff --git a/dev/Stores/User/GnuPG.js b/dev/Stores/User/GnuPG.js
index 174686d9d..f70b9a3c3 100644
--- a/dev/Stores/User/GnuPG.js
+++ b/dev/Stores/User/GnuPG.js
@@ -12,6 +12,8 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases';
+import { baseCollator } from 'Common/Translator';
+
const
findGnuPGKey = (keys, query/*, sign*/) =>
keys.find(key =>
@@ -49,13 +51,20 @@ export const GnuPGUserStore = new class {
key.for = email => aEmails.includes(IDN.toASCII(email));
key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper();
+ key.askForgetPass = ko.observable(false);
+ key.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
+ key.forgetPass = () => {
+ Passphrases.delete(key);
+ key.hasStoredPass(false);
+ };
+ key.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(key));
key.remove = () => {
if (key.askDelete()) {
Remote.request('GnupgDeleteKey',
(iError, oData) => {
if (oData) {
if (iError) {
- alert(oData.ErrorMessage);
+ alert(oData.message);
} else if (oData.Result) {
isPrivate
? this.privateKeys.remove(key)
@@ -67,6 +76,7 @@ export const GnuPGUserStore = new class {
isPrivate: isPrivate
}
);
+ isPrivate && key.forgetPass();
}
};
if (isPrivate) {
@@ -75,7 +85,7 @@ export const GnuPGUserStore = new class {
'GnuPG key
' + key.id + ' ' + key.emails[0],
btnTxt
);
- pass && pass.remember && Passphrases.set(key, pass.password);
+ pass && pass.remember && Passphrases.handle(key, pass.password);
return pass?.password;
};
}
@@ -106,7 +116,7 @@ export const GnuPGUserStore = new class {
key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key]));
return key;
},
- collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
+ collator = baseCollator(),
sort = keys => keys.sort(
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
);
@@ -163,8 +173,8 @@ export const GnuPGUserStore = new class {
return fingerprints;
}
- getPrivateKeyFor(query, sign) {
- return findGnuPGKey(this.privateKeys, query, sign);
+ getPrivateKeyFor(query/*, sign*/) {
+ return findGnuPGKey(this.privateKeys, query/*, sign*/);
}
async decrypt(message) {
diff --git a/dev/Stores/User/Identity.js b/dev/Stores/User/Identity.js
index 224ae2226..94f5ce871 100644
--- a/dev/Stores/User/Identity.js
+++ b/dev/Stores/User/Identity.js
@@ -1,5 +1,12 @@
-import { koArrayWithDestroy } from 'External/ko';
+import { koArrayWithDestroy, koComputable } from 'External/ko';
+import { isArray } from 'Common/Utils';
export const IdentityUserStore = koArrayWithDestroy();
IdentityUserStore.loading = ko.observable(false).extend({ debounce: 100 });
+
+/** Returns main (login) identity */
+IdentityUserStore.main = koComputable(() => {
+ const list = IdentityUserStore();
+ return isArray(list) ? list.find(item => item && !item.id()) : null;
+});
diff --git a/dev/Stores/User/Messagelist.js b/dev/Stores/User/Messagelist.js
index b160626b1..8afda5478 100644
--- a/dev/Stores/User/Messagelist.js
+++ b/dev/Stores/User/Messagelist.js
@@ -32,8 +32,11 @@ import { SettingsGet } from 'Common/Globals';
import { SUB_QUERY_PREFIX } from 'Common/Links';
import { AppUserStore } from 'Stores/User/App';
+import { baseCollator } from 'Common/Translator';
+
const
isChecked = item => item.checked(),
+ isDeleted = item => item.isDeleted(),
replaceHash = hash => {
rl.route.off();
hasher.replaceHash(hash);
@@ -105,9 +108,8 @@ addComputablesTo(MessagelistUserStore, {
listCheckedOrSelected: () => {
const
selectedMessage = MessagelistUserStore.selectedMessage(),
- focusedMessage = MessagelistUserStore.focusedMessage(),
checked = MessagelistUserStore.filter(item => isChecked(item));
- return checked.length ? checked : (selectedMessage || focusedMessage ? [selectedMessage || focusedMessage] : []);
+ return checked.length ? checked : (selectedMessage ? [selectedMessage] : []);
},
listCheckedOrSelectedUidsWithSubMails: () => {
@@ -135,11 +137,18 @@ MessagelistUserStore.hasChecked = koComputable(
MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
!!MessagelistUserStore.selectedMessage()
- | !!MessagelistUserStore.focusedMessage()
// Issue: not all are observed?
| !!MessagelistUserStore.find(isChecked)
).extend({ rateLimit: 50 });
+MessagelistUserStore.hasCheckedOrSelectedAndDeleted = koComputable(
+ () => !!MessagelistUserStore.listCheckedOrSelected().find(isDeleted)
+).extend({ rateLimit: 50 });
+
+MessagelistUserStore.hasCheckedOrSelectedAndUndeleted = koComputable(
+ () => !!MessagelistUserStore.listCheckedOrSelected().find(item => !item?.isDeleted())
+).extend({ rateLimit: 50 });
+
MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
if (getFolderInboxName() === folder && arrayLength(newMessages)) {
@@ -254,17 +263,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let flags = folderInfo.permanentFlags || [];
if (flags.includes('\\*')) {
+ /** Add Thunderbird labels */
let i = 6;
while (--i) {
flags.includes('$label'+i) || flags.push('$label'+i);
}
+ /** TODO: add others by default? */
}
- flags.sort((a, b) => {
- a = a.toUpperCase();
- b = b.toUpperCase();
- return (a < b) ? -1 : ((a > b) ? 1 : 0);
- });
- folder.permanentFlags(flags);
+ folder.permanentFlags(flags.sort(baseCollator().compare));
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
}
@@ -314,6 +320,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.useThreads = 1;
+ params.threadAlgorithm = SettingsUserStore.threadAlgorithm();
params.threadUid = MessagelistUserStore.threadUid();
} else {
params.threadUid = 0;
@@ -370,6 +377,14 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
messages.forEach(oMessage =>
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged')
);
+ } else if (iSetAction == MessageSetAction.SetDeleted) {
+ messages.forEach(oMessage =>
+ !oMessage.isDeleted() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\deleted')
+ );
+ } else if (iSetAction == MessageSetAction.UnsetDeleted) {
+ messages.forEach(oMessage =>
+ oMessage.isDeleted() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\deleted')
+ );
}
rootUids = rootUids.validUnique();
length = rootUids.length;
@@ -399,6 +414,15 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
setAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
});
break;
+
+ case MessageSetAction.SetDeleted:
+ case MessageSetAction.UnsetDeleted:
+ Remote.request('MessageSetDeleted', null, {
+ folder: sFolderFullName,
+ uids: rootUids.join(','),
+ setAction: iSetAction == MessageSetAction.SetDeleted ? 1 : 0
+ });
+ break;
// no default
}
}
@@ -444,23 +468,6 @@ MessagelistUserStore.moveMessages = (
if (page > MessagelistUserStore.pageCount()) {
setPage = MessagelistUserStore.pageCount();
}
- if (MessagelistUserStore.threadUid()
- && MessagelistUserStore.length
- && MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
- ) {
- const message = MessagelistUserStore.find(item => item && !item.deleted());
- if (!message) {
- if (1 < page) {
- setPage = page - 1;
- } else {
- MessagelistUserStore.threadUid(0);
- setPage = MessagelistUserStore.pageBeforeThread();
- }
- } else if (MessagelistUserStore.threadUid() != message.uid) {
- MessagelistUserStore.threadUid(message.uid);
- setPage = page;
- }
- }
if (setPage) {
MessagelistUserStore.page(setPage);
replaceHash(
@@ -518,7 +525,6 @@ MessagelistUserStore.moveMessages = (
currentMessage = null;
MessageUserStore.message(null);
}
- item.deleted(true);
MessagelistUserStore.remove(item);
});
}
diff --git a/dev/Stores/User/OpenPGP.js b/dev/Stores/User/OpenPGP.js
index b923c3de0..5c521d214 100644
--- a/dev/Stores/User/OpenPGP.js
+++ b/dev/Stores/User/OpenPGP.js
@@ -13,7 +13,11 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases';
+import { baseCollator, getNotification } from 'Common/Translator';
+
const
+ loaded = () => !!window.openpgp,
+
findOpenPGPKey = (keys, query/*, sign*/) =>
keys.find(key =>
key.for(query) || query == key.id || query == key.fingerprint
@@ -34,12 +38,12 @@ const
privateKey: privateKey.key,
passphrase
});
- result && pass.remember && Passphrases.set(privateKey, passphrase);
+ result && pass.remember && Passphrases.handle(privateKey, passphrase);
return result;
}
},
- collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
+ collator = baseCollator(),
sort = keys => keys.sort(
// (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.fingerprint, b.fingerprint)
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
@@ -54,10 +58,9 @@ const
*/
publicKeysItem = 'openpgp-public-keys',
privateKeysItem = 'openpgp-private-keys',
- storage = window.localStorage,
loadOpenPgpKeys = async itemname => {
let keys = [], key,
- armoredKeys = JSON.parse(storage.getItem(itemname)),
+ armoredKeys = JSON.parse(localStorage.getItem(itemname)),
i = arrayLength(armoredKeys);
while (i--) try {
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
@@ -70,9 +73,9 @@ const
storeOpenPgpKeys = (keys, section) => {
let armoredKeys = keys.map(item => item.armor);
if (armoredKeys.length) {
- storage.setItem(section, JSON.stringify(armoredKeys));
+ localStorage.setItem(section, JSON.stringify(armoredKeys));
} else {
- storage.removeItem(section);
+ localStorage.removeItem(section);
}
};
@@ -87,6 +90,9 @@ class OpenPgpKeyModel {
this.armor = armor;
this.askDelete = ko.observable(false);
this.openForDeletion = ko.observable(null).askDeleteHelper();
+ this.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(this));
+ this.askForgetPass = ko.observable(false);
+ this.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
// key.getUserIDs()
// key.getPrimaryUser()
}
@@ -112,12 +118,29 @@ class OpenPgpKeyModel {
if (this.key.isPrivate()) {
OpenPGPUserStore.privateKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
+ this.forgetPass()
} else {
OpenPGPUserStore.publicKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem);
}
+ Remote.request('DeletePGPKey',
+ (iError, oData) => {
+ if (oData) {
+ if (iError || oData.Result === false) {
+ alert(oData.message || getNotification(iError));
+ }
+ }
+ }, {
+ key: this.armor
+ }
+ );
}
}
+
+ forgetPass() {
+ Passphrases.delete(this);
+ this.hasStoredPass(false);
+ }
/*
toJSON() {
return this.armor;
@@ -132,23 +155,36 @@ export const OpenPGPUserStore = new class {
}
loadKeyrings() {
- if (window.openpgp) {
- loadOpenPgpKeys(publicKeysItem).then(keys => {
+ if (loaded()) {
+ loadOpenPgpKeys(publicKeysItem)
+ .then(keys => {
this.publicKeys(dedup(keys));
console.log('openpgp.js public keys loaded');
- });
- loadOpenPgpKeys(privateKeysItem).then(keys => {
- this.privateKeys(dedup(keys));
- console.log('openpgp.js private keys loaded');
+ })
+ .finally(() => {
+ loadOpenPgpKeys(privateKeysItem)
+ .then(keys => {
+ this.privateKeys(dedup(keys));
+ console.log('openpgp.js private keys loaded');
+ })
+ .finally(() => {
+ /*SettingsGet('loadBackupKeys') && */this.loadBackupKeys();
+ });
});
}
}
+ loadBackupKeys() {
+ Remote.request('GetPGPKeys',
+ (iError, oData) => !iError && oData.Result && this.importKeys(oData.Result)
+ );
+ }
+
/**
* @returns {boolean}
*/
isSupported() {
- return !!window.openpgp;
+ return loaded();
}
importKey(armoredKey) {
@@ -156,7 +192,7 @@ export const OpenPGPUserStore = new class {
}
async importKeys(keys) {
- if (window.openpgp) {
+ if (loaded()) {
const privateKeys = this.privateKeys(),
publicKeys = this.publicKeys();
for (const armoredKey of keys) try {
@@ -183,7 +219,7 @@ export const OpenPGPUserStore = new class {
keyPair.revocationCertificate
*/
storeKeyPair(keyPair) {
- if (window.openpgp) {
+ if (loaded()) {
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
diff --git a/dev/Stores/User/Pgp.js b/dev/Stores/User/Pgp.js
index 8b56a83ca..648843eea 100644
--- a/dev/Stores/User/Pgp.js
+++ b/dev/Stores/User/Pgp.js
@@ -61,7 +61,7 @@ export const
if (gnuPG && oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
GnuPGUserStore.loadKeyrings();
}
- iError && alert(oData.ErrorMessage);
+ iError && alert(oData.message);
}, {
key, gnuPG, backup
}
diff --git a/dev/Stores/User/SMime.js b/dev/Stores/User/SMime.js
index 70fa2043c..2bd4f91c8 100644
--- a/dev/Stores/User/SMime.js
+++ b/dev/Stores/User/SMime.js
@@ -1,4 +1,5 @@
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
+import { baseCollator } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
export const SMimeUserStore = koArrayWithDestroy();
@@ -12,7 +13,7 @@ SMimeUserStore.loadCertificates = () => {
SMimeUserStore.loading(true);
Remote.request('SMimeGetCertificates', (iError, oData) => {
SMimeUserStore.loading(false);
- const collator = new Intl.Collator(undefined, {sensitivity: 'base'});
+ const collator = baseCollator();
iError || SMimeUserStore(oData.Result.sort(
(a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t)
));
diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js
index 9c577715e..841e89302 100644
--- a/dev/Stores/User/Settings.js
+++ b/dev/Stores/User/Settings.js
@@ -5,6 +5,7 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { pInt } from 'Common/Utils';
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme';
+import { FolderUserStore } from 'Stores/User/Folder';
export const SettingsUserStore = new class {
constructor() {
@@ -22,6 +23,7 @@ export const SettingsUserStore = new class {
allowStyles: 0,
collapseBlockquotes: 1,
maxBlockquotesLevel: 0,
+ minFontSize: 0,
listInlineAttachments: 0,
simpleAttachmentsList: 0,
useCheckboxesInList: 1,
@@ -29,11 +31,14 @@ export const SettingsUserStore = new class {
showNextMessage: 0,
allowDraftAutosave: 1,
useThreads: 0,
+ defaultSort: '',
+ threadAlgorithm: '',
replySameFolder: 0,
hideUnsubscribed: 0,
hideDeleted: 1,
unhideKolabFolders: 0,
autoLogout: 0,
+ keyPassForget: 15,
showUnreadCount: 0,
messageNewWindow: 0,
messageReadAuto: 0,
@@ -48,6 +53,7 @@ export const SettingsUserStore = new class {
layout: 1,
editorDefaultType: 'Html',
editorWysiwyg: 'Squire',
+ markdown: 0,
msgDefaultAction: 1
});
@@ -93,13 +99,16 @@ export const SettingsUserStore = new class {
'AllowStyles',
'CollapseBlockquotes',
'MaxBlockquotesLevel',
+ 'minFontSize',
'ListInlineAttachments',
'simpleAttachmentsList',
'UseCheckboxesInList',
'listGrouped',
'showNextMessage',
'AllowDraftAutosave',
- 'UseThreads',
+ 'useThreads',
+ 'defaultSort',
+ 'threadAlgorithm',
'ReplySameFolder',
'HideUnsubscribed',
'HideDeleted',
@@ -110,7 +119,41 @@ export const SettingsUserStore = new class {
'requireTLS',
'pgpSign',
'pgpEncrypt',
- 'allowSpellcheck'
+ 'allowSpellcheck',
+ 'markdown'
+/*
+ 'MessagesPerPage',
+ 'MessageReadDelay',
+ 'SoundNotification',
+ 'NotificationSound',
+ 'DesktopNotifications',
+ 'Layout',
+ 'AutoLogout',
+ 'ContactsAutosave',
+ 'contactsAllowed',
+ 'CheckMailInterval',
+ 'SentFolder',
+ 'DraftsFolder',
+ 'JunkFolder',
+ 'TrashFolder',
+ 'ArchiveFolder',
+ 'hourCycle',
+ 'Resizer4Width',
+ 'Resizer5Width',
+ 'Resizer5Height',
+ 'fontSansSerif',
+ 'fontSerif',
+ 'fontMono',
+ 'userBackgroundName',
+ 'userBackgroundHash',
+ 'autoVerifySignatures',
+ 'allowLanguagesOnSettings',
+ 'attachmentLimit',
+ 'Theme',
+ 'language',
+ 'clientLanguage',
+ 'StaticLibsJs',
+*/
].forEach(name => {
let value = SettingsGet(name);
name = name[0].toLowerCase() + name.slice(1);
@@ -122,5 +165,8 @@ export const SettingsUserStore = new class {
self.checkMailInterval(pInt(SettingsGet('CheckMailInterval')));
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
self.autoLogout(pInt(SettingsGet('AutoLogout')));
+ self.keyPassForget(pInt(SettingsGet('keyPassForget')));
+
+ FolderUserStore.sortMode(self.defaultSort());
}
};
diff --git a/dev/Styles/Admin/Plugin.less b/dev/Styles/Admin/Plugin.less
index 9459a1c5a..c42d4f3c9 100644
--- a/dev/Styles/Admin/Plugin.less
+++ b/dev/Styles/Admin/Plugin.less
@@ -34,4 +34,9 @@
display: block;
}
+ .tab-content {
+ grid-column-end: 3;
+ border: 1px solid var(--border-color, #ddd);
+ padding: 10px;
+ }
}
diff --git a/dev/Styles/Languages.less b/dev/Styles/Languages.less
index ae5b95eb7..e0046710d 100644
--- a/dev/Styles/Languages.less
+++ b/dev/Styles/Languages.less
@@ -1,6 +1,6 @@
#V-PopupsLanguages {
- max-width: 710px;
+ max-width: 720px;
label {
display: inline-block;
diff --git a/dev/Styles/User/Compose.less b/dev/Styles/User/Compose.less
index 41c3410c8..56eae3dd1 100644
--- a/dev/Styles/User/Compose.less
+++ b/dev/Styles/User/Compose.less
@@ -9,7 +9,8 @@
#V-PopupsCompose {
height: 100vh;
height: 100dvh;
- max-width: 1000px;
+ max-width: min(99vw, 1600px);
+ resize: horizontal;
width: 99%;
.modal-body {
@@ -21,12 +22,18 @@
.tabs {
height: 100%;
+ max-height: 100%;
+ overflow: auto;
}
.tabs > label > * + * {
margin-left: 0.5em;
}
+ .tabs .tab-content {
+ overflow: auto;
+ }
+
.textAreaParent, .attachmentAreaParent {
height: 100%;
min-height: 200px;
diff --git a/dev/Styles/User/Contacts.less b/dev/Styles/User/Contacts.less
index 11d0bb87b..d9744e907 100644
--- a/dev/Styles/User/Contacts.less
+++ b/dev/Styles/User/Contacts.less
@@ -125,8 +125,9 @@
/* Right */
.right {
- width: 100%;
border-left: 1px solid var(--border-color, #ddd);
+ max-width: 600px;
+ width: 100%;
}
.b-view-content-toolbar {
diff --git a/dev/Styles/User/FolderList.less b/dev/Styles/User/FolderList.less
index 6157ec2e1..d920dfd90 100644
--- a/dev/Styles/User/FolderList.less
+++ b/dev/Styles/User/FolderList.less
@@ -82,7 +82,7 @@
border-left: 3px solid transparent;
color: var(--folders-color, #333);
display: block;
- line-height: 34px;
+ line-height: 2.2em;
padding: 0 2em 0 @folderItemPadding;
position: relative;
text-decoration: none;
@@ -151,7 +151,7 @@
color: var(--unread-count-color, #fff);
font-size: 80%;
line-height: 1.5em;
- margin-top: 7px;
+ margin-top: 0.5em;
min-width: 1.7em;
padding: 1px 4px;
text-align: center;
diff --git a/dev/Styles/User/MessageList.less b/dev/Styles/User/MessageList.less
index b16ff648a..93130fcf0 100644
--- a/dev/Styles/User/MessageList.less
+++ b/dev/Styles/User/MessageList.less
@@ -207,10 +207,6 @@
font-weight: bolder;
}
- &.deleted {
- opacity: .3;
- }
-
.messageCheckbox {
font-size: 114%;
padding: 5px .5em 0;
@@ -327,6 +323,7 @@
.attachmentParent,
.flagParent {
margin: 0 10px 0 5px;
+ min-width: 1em;
}
.flagParent::after {
content: '☆'; /*⚐*/
diff --git a/dev/Styles/User/SettingsAccounts.less b/dev/Styles/User/SettingsAccounts.less
index 54e58c288..5b7f5c91a 100644
--- a/dev/Styles/User/SettingsAccounts.less
+++ b/dev/Styles/User/SettingsAccounts.less
@@ -45,4 +45,8 @@
content: ')';
}
+ .identities-list tr + tr .identity-default {
+ display: none;
+ }
+
}
diff --git a/dev/Styles/User/SquireUI.less b/dev/Styles/User/SquireUI.less
index 646ee4dd1..1a9c46ce3 100644
--- a/dev/Styles/User/SquireUI.less
+++ b/dev/Styles/User/SquireUI.less
@@ -56,6 +56,12 @@
.squire-wysiwyg {
font-family: var(--fontSans);
+ /* Prevent collapse of empty elements so that the caret can be placed in there
+ *:empty::before {
+ content: "\200B";
+ }
+ */
+
ul {
padding-left: 40px;
li {
diff --git a/dev/Styles/_End.less b/dev/Styles/_End.less
index 5ac36e0b7..75875d0f0 100644
--- a/dev/Styles/_End.less
+++ b/dev/Styles/_End.less
@@ -16,7 +16,7 @@
text-align: var(--left, left);
top: 100%;
transition: opacity 0.2s ease-in-out;
- white-space: nowrap;
+ white-space: pre;
z-index: 2001;
}
@media screen and (max-width: @maxMobileWidth) {
@@ -33,6 +33,11 @@ input:-webkit-autofill:active {
transition: color 9999s ease-out, background-color 9999s ease-out;
}
+.btn-rotate,
+[data-icon].rotate::before {
+ transform: rotate(-180deg);
+}
+
/*
@media (pointer: coarse), (hover: none) {
[title] {
diff --git a/dev/View/Popup/Account.js b/dev/View/Popup/Account.js
index e79cb8116..8e36b14f4 100644
--- a/dev/View/Popup/Account.js
+++ b/dev/View/Popup/Account.js
@@ -36,7 +36,7 @@ export class AccountPopupView extends AbstractViewPopup {
this.submitRequest(false);
if (iError) {
this.submitError(getNotification(iError));
- this.submitErrorAdditional(data?.ErrorMessageAdditional);
+ this.submitErrorAdditional(data?.messageAdditional);
} else {
loadAccountsAndIdentities();
this.close();
diff --git a/dev/View/Popup/AdvancedSearch.js b/dev/View/Popup/AdvancedSearch.js
index 728868ffc..813023148 100644
--- a/dev/View/Popup/AdvancedSearch.js
+++ b/dev/View/Popup/AdvancedSearch.js
@@ -18,7 +18,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
text: '',
keyword: '',
repliedValue: -1,
- selectedDateValue: -1,
+ selectedDateValue: 0,
selectedTreeValue: '',
hasAttachment: false,
@@ -32,14 +32,12 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
// Almost the same as MessageModel.tagOptions
keywords: () => {
const keywords = [{value:'',label:''}];
- FolderUserStore.currentFolder().permanentFlags.forEach(value => {
- if (isAllowedKeyword(value)) {
- let lower = value.toLowerCase();
- keywords.push({
- value: value,
- label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
- });
- }
+ FolderUserStore.currentFolder().optionalTags().forEach(value => {
+ let lower = value.toLowerCase();
+ keywords.push({
+ value: value,
+ label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
+ });
});
return keywords
},
@@ -57,15 +55,25 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
selectedDates: () => {
translateTrigger();
- let prefix = 'SEARCH/DATE_';
+ // We should think about migrating all SEARCH/DATE_ to either SEARCH/SINCE_DATE or SEARCH/BEFORE_DATE
+ // and adjust the prefix accordingly
+ // let prefix_since = 'SEARCH/SINCE_DATE_';
+ let prefix = 'SEARCH/SINCE_';
+ let prefix_before = 'SEARCH/BEFORE_';
return [
- { id: -1, name: i18n(prefix + 'ALL') },
+ { id: 0, name: i18n('SEARCH/DATE_ALL') },
{ id: 3, name: i18n(prefix + '3_DAYS') },
{ id: 7, name: i18n(prefix + '7_DAYS') },
{ id: 30, name: i18n(prefix + 'MONTH') },
{ id: 90, name: i18n(prefix + '3_MONTHS') },
{ id: 180, name: i18n(prefix + '6_MONTHS') },
- { id: 365, name: i18n(prefix + 'YEAR') }
+ { id: 365, name: i18n(prefix + 'YEAR') },
+ { id: -3, name: i18n(prefix_before + '3_DAYS') },
+ { id: -7, name: i18n(prefix_before + '7_DAYS') },
+ { id: -30, name: i18n(prefix_before + 'MONTH') },
+ { id: -90, name: i18n(prefix_before + '3_MONTHS') },
+ { id: -180, name: i18n(prefix_before + '6_MONTHS') },
+ { id: -365, name: i18n(prefix_before + 'YEAR') }
];
},
@@ -102,11 +110,16 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
append('text', self.text().trim());
append('keyword', self.keyword());
append('in', self.selectedTreeValue());
- if (-1 < self.selectedDateValue()) {
+ if (0 < self.selectedDateValue()) {
let d = new Date();
d.setDate(d.getDate() - self.selectedDateValue());
append('since', d.toISOString().split('T')[0]);
}
+ else if (-1 > self.selectedDateValue()) {
+ let d = new Date();
+ d.setDate(d.getDate() + self.selectedDateValue());
+ append('before', d.toISOString().split('T')[0]);
+ }
let result = decodeURIComponent(new URLSearchParams(data).toString());
@@ -138,8 +151,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
self.text(pString(params.get('text')));
self.keyword(pString(params.get('keyword')));
self.selectedTreeValue(pString(params.get('in')));
-// self.selectedDateValue(params.get('since'));
- self.selectedDateValue(-1);
+ self.selectedDateValue(0);
self.hasAttachment(params.has('attachment'));
self.starred(params.has('flagged'));
self.unseen(params.has('unseen'));
diff --git a/dev/View/Popup/Ask.js b/dev/View/Popup/Ask.js
index 8d7c7bca0..8392dfcbd 100644
--- a/dev/View/Popup/Ask.js
+++ b/dev/View/Popup/Ask.js
@@ -15,10 +15,14 @@ export class AskPopupView extends AbstractViewPopup {
askUsername: false,
passphrase: '',
askPass: false,
- remember: true,
- askRemeber: false
+ remember: true, // remember for session
+ askRemeber: false,
+ rememberPermanent: false,
+ askRememberPermanent: false,
});
+ this.rememberPermanent.subscribe(value => value && this.remember(true));
+
this.fYesAction = null;
this.fNoAction = null;
@@ -48,10 +52,12 @@ export class AskPopupView extends AbstractViewPopup {
this.askDesc(sAskDesc || '');
this.askUsername(ask & 2);
this.askPass(ask & 1);
- this.askRemeber(ask & 4);
+ this.askRemeber(ask & 4); // 0b0100
+ this.askRememberPermanent(ask & 0b1000)
this.username('');
this.passphrase('');
this.remember(true);
+ this.rememberPermanent(false);
this.yesButton(i18n(btnText || 'GLOBAL/YES'));
this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
this.fYesAction = fYesFunc;
diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js
index cb0010873..ffa49a2a4 100644
--- a/dev/View/Popup/Compose.js
+++ b/dev/View/Popup/Compose.js
@@ -44,6 +44,7 @@ import Remote from 'Remote/User/Fetch';
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
import { EmailModel } from 'Model/Email';
+import { IdentityModel } from 'Model/Identity';
import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { addressparser } from 'Mime/Address';
@@ -92,7 +93,7 @@ const
findIdentity = addresses => {
addresses = addresses.map(item => item.email);
- return IdentityUserStore.find(item => addresses.includes(item.email()));
+ return IdentityUserStore.find(item => addresses.includes(item.email));
},
/**
@@ -298,7 +299,7 @@ export class ComposePopupView extends AbstractViewPopup {
]
});
- this.tryToClose = this.tryToClose.debounce(200);
+ this.doClose = this.doClose.debounce(200);
this.iTimer = 0;
@@ -336,11 +337,14 @@ export class ComposePopupView extends AbstractViewPopup {
canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
canSign: () => this.signOptions().length,
+ encryptOptionsText: () => this.encryptOptions().join(', '),
+ signOptionsText: () => this.signOptions().map(o => o[0]).join(', '),
+
identitiesOptions: () =>
IdentityUserStore.map(item => ({
item: item,
optValue: item.id(),
- optText: item.formattedName()
+ optText: item
})),
canBeSentOrSaved: () => !this.sending() && !this.saving()
@@ -355,7 +359,7 @@ export class ComposePopupView extends AbstractViewPopup {
currentIdentity: value => {
if (value) {
- this.from(value.formattedName());
+ this.from(value.toString());
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
}
@@ -451,13 +455,24 @@ export class ComposePopupView extends AbstractViewPopup {
contactsCommand: self => self.allowContacts
});
- this.from(IdentityUserStore()[0].formattedName());
+ this.from(IdentityUserStore()[0].toString());
+ }
+
+ sentFolder()
+ {
+ let sSentFolder = this.currentIdentity()?.sentFolder?.() || FolderUserStore.sentFolder();
+ if (SettingsUserStore.replySameFolder()) {
+ if (
+ 3 === arrayLength(this.aDraftInfo) &&
+ this.aDraftInfo[2]?.length
+ ) {
+ sSentFolder = this.aDraftInfo[2];
+ }
+ }
+ return UNUSED_OPTION_VALUE === sSentFolder ? null : sSentFolder;
}
sendCommand() {
- const identity = this.currentIdentity();
- let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
-
this.attachmentsInProcessError(false);
this.attachmentsInErrorError(false);
this.emptyToError(false);
@@ -475,80 +490,95 @@ export class ComposePopupView extends AbstractViewPopup {
}
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
- if (SettingsUserStore.replySameFolder()) {
- if (
- 3 === arrayLength(this.aDraftInfo) &&
- null != this.aDraftInfo[2] &&
- this.aDraftInfo[2].length
- ) {
- sSentFolder = this.aDraftInfo[2];
- }
- }
-
- if (!sSentFolder) {
+ const sSentFolder = this.sentFolder();
+ if ('' === sSentFolder) {
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
- } else try {
- this.sendError(false);
- this.sending(true);
-
- sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
-
- this.getMessageRequestParams(sSentFolder).then(params => {
- Remote.request('SendMessage',
- (iError, data) => {
- this.sending(false);
- if (iError) {
- if (Notifications.CantSaveMessage === iError) {
- this.sendSuccessButSaveError(true);
- let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
- if (data?.ErrorMessageAdditional) {
- msg = msg + "\n" + data?.ErrorMessageAdditional;
- }
- this.savedErrorDesc(msg);
- } else {
- params.signPassphrase && Passphrases.delete(identity);
- this.sendError(true);
- this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
- || getNotification(Notifications.CantSendMessage));
- }
- } else {
- if (arrayLength(this.aDraftInfo) > 0) {
- const flag = {
- 'reply': '\\answered',
- 'forward': '$forwarded'
- }[this.aDraftInfo[0]];
- if (flag) {
- const aFlags = oLastMessage.flags();
- if (aFlags.indexOf(flag) === -1) {
- aFlags.push(flag);
- oLastMessage.flags(aFlags);
- }
- }
- }
- this.close();
- }
- setFolderETag(this.draftsFolder(), '');
- setFolderETag(sSentFolder, '');
- if (3 === arrayLength(this.aDraftInfo)) {
- const folder = this.aDraftInfo[2];
- setFolderETag(folder, '');
- }
- reloadDraftFolder();
- },
- params,
- 30000
- );
- }).catch(e => {
+ } else {
+ const sendError = e => {
console.error(e);
this.sendError(true);
this.sendErrorDesc(e);
this.sending(false);
- });
- } catch (e) {
- console.error(e);
- this.sendError(true);
- this.sendErrorDesc(e);
- this.sending(false);
+ };
+ const sendFailed = (iError, data) => {
+ this.sendError(true);
+ this.sendErrorDesc(
+ getNotification(iError, data?.message, Notifications.CantSendMessage)
+ + "\n" + (data?.messageAdditional || data?.message)
+ );
+ };
+ try {
+ this.sendError(false);
+ this.sending(true);
+
+ const sendMessage = params => {
+ Remote.request('SendMessage',
+ (iError, data) => {
+ this.sending(false);
+ if (iError) {
+/*
+ if (Notifications.AuthError === iError && !params.auth) {
+ AskPopupView.password('SMTP login', 'retry', 3).then(result => {
+ if (result) {
+ this.sending(true);
+ params.auth = result;
+ sendMessage(params);
+ } else {
+ sendFailed(iError, data);
+ }
+ });
+ } else
+*/
+ if (Notifications.CantSaveMessage === iError) {
+ this.sendSuccessButSaveError(true);
+ let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
+ if (data?.messageAdditional) {
+ msg = msg + "\n" + data?.messageAdditional;
+ }
+ this.savedErrorDesc(msg);
+ } else {
+ this.sendError(true);
+ sendFailed(iError, data);
+ // Remove remembered passphrase as it could be wrong
+ let key = ('S/MIME' === params.sign) ? this.currentIdentity() : null;
+ params.signFingerprint
+ && this.signOptions.forEach(option => ('GnuPG' === option[0]) && (key = option[1]));
+ key && Passphrases.delete(key);
+ }
+ } else {
+ if (arrayLength(this.aDraftInfo) > 0) {
+ const flag = {
+ 'reply': '\\answered',
+ 'forward': '$forwarded'
+ }[this.aDraftInfo[0]];
+ if (flag) {
+ const aFlags = oLastMessage.flags();
+ if (aFlags.indexOf(flag) === -1) {
+ aFlags.push(flag);
+ oLastMessage.flags(aFlags);
+ }
+ }
+ }
+ this.close();
+ }
+ setFolderETag(this.draftsFolder(), '');
+ setFolderETag(params.saveFolder, '');
+ if (3 === arrayLength(this.aDraftInfo)) {
+ setFolderETag(this.aDraftInfo[2], '');
+ }
+ reloadDraftFolder();
+ },
+ params,
+ 30000
+ );
+ };
+
+ this.getMessageRequestParams(sSentFolder)
+ .then(sendMessage)
+ .catch(sendError);
+ } catch (e) {
+ sendError(e);
+ }
}
}
}
@@ -636,7 +666,7 @@ export class ComposePopupView extends AbstractViewPopup {
this.saveCommand();
}
- this.tryToClose();
+ this.doClose();
}
contactsCommand() {
@@ -833,6 +863,7 @@ export class ComposePopupView extends AbstractViewPopup {
case ComposeType.Forward:
case ComposeType.ForwardAsAttachment:
identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc))
+ || findIdentity(oLastMessage.from)
/* || findIdentity(oLastMessage.deliveredTo)*/;
break;
case ComposeType.Draft:
@@ -842,10 +873,20 @@ export class ComposePopupView extends AbstractViewPopup {
// case ComposeType.Empty:
}
}
+ // Set from custom email
+ if (!identity
+ && oLastMessage && (ComposeType.Reply === options.mode || ComposeType.ReplyAll === options.mode)
+ && 1 === oLastMessage.to.length
+// && mEmail.includes(oLastMessage.to[0].domain)
+ ) {
+ identity = new IdentityModel;
+ identity.name = oLastMessage.to[0].name;
+ identity.email = oLastMessage.to[0].email;
+ }
identity = identity || IdentityUserStore()[0];
if (identity) {
-// excludeEmail.add(identity.email());
- excludeEmail[identity.email()] = true;
+// excludeEmail.add(identity.email);
+ excludeEmail[identity.email] = true;
}
if (arrayLength(options.to)) {
@@ -861,7 +902,7 @@ export class ComposePopupView extends AbstractViewPopup {
}
if (options.mode && oLastMessage) {
- let encrypted,
+ let usePlain,
sCc = '',
sDate = timestampToString(oLastMessage.dateTimestamp(), 'FULL'),
sSubject = oLastMessage.subject(),
@@ -889,10 +930,9 @@ export class ComposePopupView extends AbstractViewPopup {
oLastMessage.headers().valuesByName('autocrypt').forEach(value => {
let autocrypt = new MimeHeaderAutocryptModel(value);
if (autocrypt.addr && autocrypt.keydata) {
- PgpUserStore.hasPublicKeyForEmails([autocrypt.addr]).then(result =>
- result || PgpUserStore.importKey(autocrypt.pem(), true, true)
-// result || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
- );
+ PgpUserStore.hasPublicKeyForEmails([autocrypt.addr])
+ || PgpUserStore.importKey(autocrypt.pem(), true, true)
+// || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
}
});
} break;
@@ -931,7 +971,7 @@ export class ComposePopupView extends AbstractViewPopup {
// https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = oLastMessage.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img => {
- img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
+ img.src || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
});
sText = tpl.innerHTML.trim();
@@ -970,18 +1010,14 @@ export class ComposePopupView extends AbstractViewPopup {
break;
default:
- encrypted = PgpUserStore.isEncrypted(sText);
- if (encrypted) {
+ usePlain = PgpUserStore.isEncrypted(sText) || isPlainEditor() || !oLastMessage.isHtml();
+ if (usePlain) {
sText = oLastMessage.plain();
}
}
this.editor(editor => {
- encrypted || editor.setHtml(sText);
- if (encrypted || isPlainEditor()) {
- editor.modePlain();
- }
- encrypted && editor.setPlain(sText);
+ usePlain ? (editor.modePlain() | editor.setPlain(sText)) : editor.setHtml(sText);
this.setSignature(identity, options.mode);
this.setFocusInPopup();
});
@@ -1048,7 +1084,7 @@ export class ComposePopupView extends AbstractViewPopup {
}, 100);
}
- tryToClose() {
+ doClose() {
if (AskPopupView.hidden()) {
if (ComposePopupView.inEdit() || (this.isEmptyForm() && !this.draftUid())) {
this.close();
@@ -1127,7 +1163,7 @@ export class ComposePopupView extends AbstractViewPopup {
.on('onComplete', (id, result, data) => {
const attachment = this.getAttachmentById(id),
response = data?.Result || {},
- errorCode = response.ErrorCode,
+ errorCode = response.code,
attachmentJson = result && response.Attachment;
let error = '';
@@ -1143,7 +1179,7 @@ export class ComposePopupView extends AbstractViewPopup {
.waiting(false)
.uploading(false)
.complete(true)
- .error(error + '\n' + response.ErrorMessage);
+ .error(error + '\n' + response.message);
} else if (attachmentJson) {
attachment
.waiting(false)
@@ -1194,7 +1230,7 @@ export class ComposePopupView extends AbstractViewPopup {
});
addShortcut('escape,close', 'shift', ScopeCompose, () => {
- this.tryToClose();
+ this.doClose();
return false;
});
@@ -1357,7 +1393,7 @@ export class ComposePopupView extends AbstractViewPopup {
allRecipients() {
return [
// From/sender is also recipient (Sent mailbox)
-// this.currentIdentity().email(),
+// this.currentIdentity().email,
this.from(),
this.to(),
this.cc(),
@@ -1377,7 +1413,8 @@ export class ComposePopupView extends AbstractViewPopup {
key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]);
- identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
+ identity.smimeKeyValid() && identity.smimeCertificateValid()
+ && identity.smimeCertificateChainValid() && identity.email === email
&& options.push(['S/MIME']);
console.dir({signOptions: options});
this.signOptions(options);
@@ -1394,22 +1431,22 @@ export class ComposePopupView extends AbstractViewPopup {
OpenPGPUserStore.hasPublicKeyForEmails(recipients)
&& options.push('OpenPGP');
- if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
- options.push('Mailvelope');
- } else {
- 'mailvelope' === this.viewArea() && this.bodyArea();
-// this.dropMailvelope();
- }
-
const count = recipients.length,
identity = this.currentIdentity(),
- from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
+ from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email : null;
count
&& count === recipients.filter(email =>
email == from
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
).length
&& options.push('S/MIME');
+
+ if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
+ options.push('Mailvelope');
+ } else {
+ 'mailvelope' === this.viewArea() && this.bodyArea();
+// this.dropMailvelope();
+ }
}
console.dir({encryptOptions:options});
@@ -1473,12 +1510,21 @@ export class ComposePopupView extends AbstractViewPopup {
**/
linkedData: []
},
- recipients = draft ? [identity.email()] : this.allRecipients(),
+ recipients = draft ? [identity.email] : this.allRecipients(),
signOptions = !draft && this.doSign() && this.signOptions(),
encryptOptions = this.doEncrypt() && this.encryptOptions(),
isHtml = this.oEditor.isHtml();
if (isHtml) {
+ tpl.innerHTML = Text;
+ tpl.content.querySelectorAll('img').forEach(img => {
+ if (img.dataset.xSrc) {
+ img.src = img.dataset.xSrc;
+ img.removeAttribute('data-x-src')
+ }
+ });
+ Text = tpl.innerHTML.trim();
+
do {
l = Text.length;
Text = Text
@@ -1522,12 +1568,11 @@ export class ComposePopupView extends AbstractViewPopup {
alternative.children.push(data);
data = alternative;
}
- let sign = true;
+ let isSigned = false;
for (let i = 0; i < signOptions.length; ++i) {
if ('OpenPGP' == signOptions[i][0]) {
try {
// Doesn't sign attachments
- params.html = params.plain = '';
let signed = new MimePart;
signed.headers['Content-Type'] =
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
@@ -1538,47 +1583,52 @@ export class ComposePopupView extends AbstractViewPopup {
signature.headers['Content-Transfer-Encoding'] = '7Bit';
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
signed.children.push(signature);
+ isSigned = true;
+ params.html = params.plain = '';
params.signed = signed.toString();
params.boundary = signed.boundary;
data = signed;
+/*
+ Object.entries(PgpUserStore.getPublicKeyOfEmails([getEmail(this.from())]) || {})
+ .forEach(([k,v]) => params.publicKey = v);
+*/
+ break;
} catch (e) {
- sign = false;
+ Passphrases.delete(signOptions[i][1])
console.error(e);
}
- break;
- }
- if ('GnuPG' == signOptions[i][0]) {
+ } else if ('GnuPG' == signOptions[i][0]) {
// TODO: sign in PHP fails
let pass = await GnuPGUserStore.sign(signOptions[i][1]);
if (null != pass) {
// params.signData = data.toString();
params.signFingerprint = signOptions[i][1].fingerprint;
params.signPassphrase = pass;
- } else {
- sign = false;
+// params.attachPublicKey = false;
+ isSigned = true;
+ break;
}
- break;
- }
- if ('S/MIME' == signOptions[i][0]) {
+ } else if ('S/MIME' == signOptions[i][0]) {
// TODO: sign in PHP fails
params.sign = 'S/MIME';
// params.signCertificate = identity.smimeCertificate();
+// params.signCertificateChain = identity.smimeCertificateChain();
// params.signPrivateKey = identity.smimeKey();
+// params.attachCertificate = false;
if (identity.smimeKeyEncrypted()) {
const pass = await Passphrases.ask(identity,
- i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
+ i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
'CRYPTO/SIGN'
);
if (null != pass) {
params.signPassphrase = pass.password;
-// pass.remember && Passphrases.set(identity, pass.password);
- } else {
- sign = false;
+ pass.remember && Passphrases.handle(identity, pass.password);
+ isSigned = true;
}
}
}
}
- if (signOptions.length && !sign) {
+ if (signOptions.length && !isSigned) {
throw 'Signing failed';
}
@@ -1587,7 +1637,9 @@ export class ComposePopupView extends AbstractViewPopup {
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
params.autocrypt.push({
addr: k,
- keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
+ keydata: v
+ .replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '')
+ .replace(/\s+/g, '') // Single base64 string, let the server do compliance formatting
})
);
for (let i = 0; i < encryptOptions.length; ++i) {
@@ -1607,7 +1659,7 @@ export class ComposePopupView extends AbstractViewPopup {
if ('S/MIME' == encryptOptions[i]) {
params.encryptCertificates = [identity.smimeCertificate()];
SMimeUserStore.forEach(certificate => {
- certificate.emailAddress != identity.email()
+ certificate.emailAddress != identity.email
&& recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id)
});
diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js
index 15b6d61f3..2f75d0070 100644
--- a/dev/View/Popup/Contacts.js
+++ b/dev/View/Popup/Contacts.js
@@ -116,7 +116,7 @@ export class ContactsPopupView extends AbstractViewPopup {
Remote.request('ContactsDelete',
(iError, oData) => {
if (iError) {
- alert(oData?.ErrorMessage || getNotification(iError));
+ alert(oData?.message || getNotification(iError));
} else {
const page = this.contactsPage();
if (page > Math.max(1, Math.ceil((this.contactsCount() - count) / CONTACTS_PER_PAGE))) {
@@ -181,7 +181,7 @@ export class ContactsPopupView extends AbstractViewPopup {
Remote.request('ContactSave',
(iError, oData) => {
if (iError) {
- alert(oData?.ErrorMessage || getNotification(iError));
+ alert(oData?.message || getNotification(iError));
} else if (oData.Result.ResultID) {
if (contact.id()) {
contact.id(oData.Result.ResultID);
@@ -245,7 +245,7 @@ export class ContactsPopupView extends AbstractViewPopup {
if (iError) {
// console.error(data);
- alert(data?.ErrorMessage || getNotification(iError));
+ alert(data?.message || getNotification(iError));
} else if (arrayLength(data.Result.List)) {
data.Result.List.forEach(item => {
item = ContactModel.reviveFromJson(item);
@@ -314,10 +314,6 @@ export class ContactsPopupView extends AbstractViewPopup {
}
}
- tryToClose() {
- (false === this.onClose()) || this.close();
- }
-
onClose() {
const contact = this.contact();
if (AskPopupView.hidden() && contact?.hasChanges()) {
diff --git a/dev/View/Popup/Domain.js b/dev/View/Popup/Domain.js
index 9a64d7716..764655acc 100644
--- a/dev/View/Popup/Domain.js
+++ b/dev/View/Popup/Domain.js
@@ -39,12 +39,15 @@ const
imapMessage_all_headers: false,
imapMessage_list_limit: 10000,
imapSearch_filter: '',
+ imapSpam_headers: '',
+ imapVirus_headers: '',
sieveEnabled: false,
sieveHost: '',
sievePort: 4190,
sieveType: 0,
sieveTimeout: 10,
+ sieveAuthLiteral: true,
smtpHost: '',
smtpPort: 25,
@@ -79,15 +82,17 @@ const
},
disabled_capabilities: oDomain.imapDisabled_capabilities(),
folder_list_limit: pInt(oDomain.imapFolder_list_limit()),
- message_list_limit: pInt(oDomain.imapMessage_list_limit())
+ message_list_limit: pInt(oDomain.imapMessage_list_limit()),
/*
expunge_all_on_delete: ,
fast_simple_search: ,
fetch_new_messages: ,
force_select: ,
message_all_headers: ,
- search_filter:
*/
+ search_filter: oDomain.imapSearch_filter(),
+ spam_headers: oDomain.imapSpam_headers(),
+ virus_headers: oDomain.imapVirus_headers()
},
SMTP: {
host: oDomain.smtpHost,
@@ -108,6 +113,7 @@ const
},
Sieve: {
enabled: !!oDomain.sieveEnabled(),
+ authLiteral: !!oDomain.sieveAuthLiteral(),
host: oDomain.sieveHost,
port: oDomain.sievePort,
secure: pInt(oDomain.sieveType()),
@@ -142,6 +148,8 @@ export class DomainPopupView extends AbstractViewPopup {
imapHostFocus: false,
sieveHostFocus: false,
smtpHostFocus: false,
+
+ detectingConfig: false
});
this.imapDisabled_capabilities = ko.observableArray();
this.imapCapabilities = ko.observableArray();
@@ -312,6 +320,7 @@ export class DomainPopupView extends AbstractViewPopup {
}
autoconfig() {
+ this.detectingConfig(true);
let domain = this.name();
Remote.request('AdminDomainAutoconfig', (iError, oData) => {
if (oData?.Result?.config) {
@@ -329,6 +338,7 @@ export class DomainPopupView extends AbstractViewPopup {
this.smtpUseAuth(!!server.authentication);
this.smtpUsePhpMail(false);
}
+ this.detectingConfig(false);
}, {domain});
}
@@ -347,7 +357,7 @@ export class DomainPopupView extends AbstractViewPopup {
'STATUS=SIZE',
'THREAD'
]);
- this.imapDisabled_capabilities(['METADATA','PREVIEW','STATUS=SIZE']);
+ this.imapDisabled_capabilities(['METADATA','OBJECTID','PREVIEW','STATUS=SIZE']);
forEachObjectEntry(domainDefaults, (key, value) => this[key](value));
this.enableSmartPorts(true);
if (oDomain) {
diff --git a/dev/View/Popup/Folder.js b/dev/View/Popup/Folder.js
index 4dcdb0442..6a6160e6f 100644
--- a/dev/View/Popup/Folder.js
+++ b/dev/View/Popup/Folder.js
@@ -10,6 +10,48 @@ import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n, getNotification } from 'Common/Translator';
import { defaultOptionsAfterRender } from 'Common/Utils';
+import { FolderACLPopupView } from 'View/Popup/FolderAcl';
+import { showScreenPopup } from 'Knoin/Knoin';
+
+import { AbstractCollectionModel } from 'Model/AbstractCollection';
+export class FolderACLModel extends AbstractCollectionModel
+{
+ static reviveFromJson(json) {
+ return super.reviveFromJson(json, rights => FolderACLRightsModel.reviveFromJson(rights));
+ }
+}
+
+import { AbstractModel } from 'Knoin/AbstractModel';
+export class FolderACLRightsModel extends AbstractModel {
+ constructor() {
+ super();
+ addObservablesTo(this, {
+ identifier: '',
+ mine: false
+ });
+ // The "RIGHTS=" capability MUST NOT include any of the rights defined in RFC 2086.
+ // That way we know it supports RFC 4314
+// this.rights = ko.observableArray(/*'alrswipcd'.split('')*/);
+ this.rights = ko.observableArray(/*'alrswipkxte'.split('')*/);
+ }
+
+ static reviveFromJson(json) {
+ json.rights = json.rights.split('');
+ return super.reviveFromJson(json);
+ }
+/*
+ get mayReadItems() { return this.rights.includes('l') && this.rights.includes('r'); }
+ get mayAddItems() { return this.rights.includes('i'); }
+ get mayRemoveItems() { return this.rights.includes('t') && this.rights.includes('e'); }
+ get maySetSeen() { return this.rights.includes('s'); }
+ get maySetKeywords() { return this.rights.includes('w'); }
+ get mayCreateChild() { return this.rights.includes('k'); }
+ get mayRename() { return this.rights.includes('x'); }
+ get mayDelete() { return this.rights.includes('x'); }
+ get maySubmit() { return this.rights.includes('p'); }
+*/
+}
+
export class FolderPopupView extends AbstractViewPopup {
constructor() {
super('Folder');
@@ -17,10 +59,10 @@ export class FolderPopupView extends AbstractViewPopup {
folder: null, // FolderModel
parentFolder: '',
name: '',
- editing: false
+ editing: false,
+ adminACL: false
});
this.ACLAllowed = FolderUserStore.hasCapability('ACL');
- this.ACL = ko.observableArray();
this.parentFolderSelectList = koComputable(() =>
folderListOptionsBuilder(
@@ -51,6 +93,8 @@ export class FolderPopupView extends AbstractViewPopup {
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
+ this.editACL = this.editACL.bind(this);
+ this.deleteACL = this.deleteACL.bind(this);
}
afterHide() {
@@ -135,11 +179,35 @@ export class FolderPopupView extends AbstractViewPopup {
this.close();
}
+ createACL()
+ {
+ showScreenPopup(FolderACLPopupView, [this.folder(), new FolderACLRightsModel]);
+ }
+
+ editACL(acl)
+ {
+ showScreenPopup(FolderACLPopupView, [this.folder(), acl]);
+ }
+
+ deleteACL(acl)
+ {
+ Remote.request('FolderDeleteACL',
+ (iError, data) => !iError && data.Result && this.folder().ACL.remove(acl),
+ {
+ folder: this.folder().fullName,
+ identifier: acl.identifier
+ }
+ );
+ }
+
beforeShow(folder) {
- this.ACL([]);
+ folder.ACL || (folder.ACL = ko.observableArray());
+ this.adminACL(false);
this.ACLAllowed && Remote.request('FolderACL', (iError, data) => {
if (!iError && data.Result) {
- this.ACL(Object.values(data.Result));
+ folder.ACL(FolderACLModel.reviveFromJson(data.Result));
+// data.Result.map(aItem => FolderACLRightsModel.reviveFromJson(aItem)).filter(v => v)
+ this.adminACL(folder.ACL()[0].rights.includes('a'));
}
}, {
folder: folder.fullName
diff --git a/dev/View/Popup/FolderAcl.js b/dev/View/Popup/FolderAcl.js
new file mode 100644
index 000000000..786fdb21b
--- /dev/null
+++ b/dev/View/Popup/FolderAcl.js
@@ -0,0 +1,58 @@
+import { AbstractViewPopup } from 'Knoin/AbstractViews';
+import { addObservablesTo } from 'External/ko';
+import Remote from 'Remote/User/Fetch';
+
+export class FolderACLPopupView extends AbstractViewPopup {
+ constructor() {
+ super('FolderACL');
+ addObservablesTo(this, {
+ create: false,
+ mine: false,
+ folderName: '',
+ identifier: ''
+ });
+ this.rights = ko.observableArray();
+ }
+
+ submitForm(/*form*/) {
+ if (!this.mine()) {
+ const rights = this.rights();
+ Remote.request('FolderSetACL',
+ (iError, data) => {
+ if (!iError && data.Result) {
+ const acl = this.acl;
+ if (!acl.identifier) {
+ this.folder.ACL.push(acl);
+ }
+ acl.rights = rights;
+ }
+ }, {
+ folder: this.folderName(),
+ identifier: this.identifier(),
+ rights: rights.join('')
+ }
+ );
+ }
+ this.close();
+ }
+
+ beforeShow(folder, acl) {
+ this.folder = folder;
+ this.create(!acl.identifier());
+ this.mine(acl.mine());
+ this.acl = acl;
+/*
+ this.ACLAllowed && Remote.request('FolderIdentifierRights', (iError, data) => {
+ if (!iError && data.Result) {
+ this.rights(data.Result.rights.split(''));
+ }
+ }, {
+ folder: folder.fullName,
+ identifier: acl.identifier
+ });
+*/
+ this.folderName(folder.fullName);
+ this.identifier(acl.identifier());
+ this.rights(acl.rights());
+ }
+}
diff --git a/dev/View/Popup/FolderCreate.js b/dev/View/Popup/FolderCreate.js
index a1cb4e370..0f47b0ca1 100644
--- a/dev/View/Popup/FolderCreate.js
+++ b/dev/View/Popup/FolderCreate.js
@@ -3,7 +3,7 @@ import { koComputable, addObservablesTo } from 'External/ko';
import { Notifications } from 'Common/Enums';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
-import { getNotification } from 'Common/Translator';
+import { getNotification/*, baseCollator*/ } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
@@ -56,7 +56,7 @@ export class FolderCreatePopupView extends AbstractViewPopup {
folders.push(subFolder);
sortFolders(folders);
/*
- var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
+ var collator = baseCollator(true);
console.log((folder ? folder.subFolders : FolderUserStore.folderList).sort(collator.compare));
*/
},
diff --git a/dev/View/Popup/Identity.js b/dev/View/Popup/Identity.js
index 97b8de9d9..b47b9d07d 100644
--- a/dev/View/Popup/Identity.js
+++ b/dev/View/Popup/Identity.js
@@ -2,7 +2,6 @@
import { addObservablesTo, koComputable } from 'External/ko';
import { getNotification } from 'Common/Translator';
-import { loadAccountsAndIdentities } from 'Common/UtilsUser';
import Remote from 'Remote/User/Fetch';
@@ -14,6 +13,7 @@ import { folderListOptionsBuilder } from 'Common/Folders';
import { i18n } from 'Common/Translator';
import { defaultOptionsAfterRender } from 'Common/Utils';
+import { showScreenPopup } from 'Knoin/Knoin';
import { AskPopupView } from 'View/Popup/Ask';
export class IdentityPopupView extends AbstractViewPopup {
@@ -42,26 +42,54 @@ export class IdentityPopupView extends AbstractViewPopup {
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.createSelfSigned = this.createSelfSigned.bind(this);
+ this.setSMimeKeyPass = this.setSMimeKeyPass.bind(this);
}
- async createSelfSigned() {
- const identity = this.identity(),
- pass = await AskPopupView.password('', 'CRYPTO/CREATE_SELF_SIGNED');
- if (pass) {
- Remote.request('SMimeCreateCertificate', (iError, oData) => {
- if (oData.Result.x509) {
- identity.smimeKey(oData.Result.pkey);
- identity.smimeCertificate(oData.Result.x509);
- } else {
- this.submitError(oData.ErrorMessage);
- }
- }, {
- name: identity.name(),
- email: identity.email(),
- privateKey: identity.smimeKey(),
- passphrase: pass.password
- });
+ createSelfSigned() {
+ AskPopupView.password('', 'CRYPTO/CREATE_SELF_SIGNED').then(result => {
+ if (result) {
+ const identity = this.identity();
+ Remote.request('SMimeCreateCertificate', (iError, oData) => {
+ if (oData.Result.x509) {
+ identity.smimeKey(oData.Result.pkey);
+ identity.smimeCertificate(oData.Result.x509);
+ } else {
+ this.submitError(oData.message);
+ }
+ }, {
+ name: identity.name,
+ email: identity.email,
+ privateKey: identity.smimeKey(),
+ passphrase: result.password
+ });
+ }
+ });
+ }
+
+ async setSMimeKeyPass() {
+ const identity = this.identity();
+ let old = null
+ if (identity.smimeKeyEncrypted()) {
+ old = await AskPopupView.password(i18n('CRYPTO/CURRENT_PASS'), 'CRYPTO/DECRYPT');
+ if (!old) {
+ return;
+ }
}
+ AskPopupView.password(i18n('CRYPTO/NEW_PASS'), 'GLOBAL/SAVE').then(result => {
+ if (result) {
+ Remote.request('SMimeExportPrivateKey', (iError, oData) => {
+ if (oData.Result) {
+ identity.smimeKey(oData.Result);
+ } else {
+ this.submitError(oData.message);
+ }
+ }, {
+ privateKey: identity.smimeKey(),
+ oldPassphrase: old?.password,
+ newPassphrase: result.password
+ });
+ }
+ });
}
submitForm(form) {
@@ -77,7 +105,7 @@ export class IdentityPopupView extends AbstractViewPopup {
if (iError) {
this.submitError(getNotification(iError));
} else {
- loadAccountsAndIdentities();
+ rl.app.loadAccountsAndIdentities();
this.close();
}
}, data
@@ -104,4 +132,14 @@ export class IdentityPopupView extends AbstractViewPopup {
afterShow() {
this.identity().id() ? this.labelFocused(true) : this.nameFocused(true);
}
+
+ onClose() {
+ if (!this.identity().exists()) {
+ showScreenPopup(AskPopupView, [
+ i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
+ () => this.close()
+ ]);
+ return false;
+ }
+ }
}
diff --git a/dev/View/Popup/OpenPgpGenerate.js b/dev/View/Popup/OpenPgpGenerate.js
index a2ec51515..e785af81e 100644
--- a/dev/View/Popup/OpenPgpGenerate.js
+++ b/dev/View/Popup/OpenPgpGenerate.js
@@ -105,9 +105,9 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
}
onShow() {
- this.name(''/*IdentityUserStore()[0].name()*/);
+ this.name(''/*IdentityUserStore()[0].name*/);
this.password('');
- this.email(''/*IdentityUserStore()[0].email()*/);
+ this.email(''/*IdentityUserStore()[0].email*/);
this.emailError(false);
this.submitError('');
}
diff --git a/dev/View/Popup/OpenPgpImport.js b/dev/View/Popup/OpenPgpImport.js
index 896cc0829..3765b38e9 100644
--- a/dev/View/Popup/OpenPgpImport.js
+++ b/dev/View/Popup/OpenPgpImport.js
@@ -35,7 +35,7 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
const fn = () => Remote.request('PgpSearchKey',
(iError, oData) => {
if (iError) {
- this.key(oData.ErrorMessage);
+ this.key(oData.message);
} else {
this.key(oData.Result);
}
diff --git a/dev/View/Popup/Plugin.js b/dev/View/Popup/Plugin.js
index 4761cc72b..ff597b915 100644
--- a/dev/View/Popup/Plugin.js
+++ b/dev/View/Popup/Plugin.js
@@ -18,7 +18,11 @@ export class PluginPopupView extends AbstractViewPopup {
saveError: '',
id: '',
name: '',
- readme: ''
+ readme: '',
+ author: '',
+ url: '',
+ version: '',
+ released: ''
});
this.config = ko.observableArray();
@@ -73,12 +77,20 @@ export class PluginPopupView extends AbstractViewPopup {
this.id('');
this.name('');
this.readme('');
+ this.author('');
+ this.url('');
+ this.version('');
+ this.released('');
this.config([]);
if (oPlugin) {
this.id(oPlugin.id);
this.name(oPlugin.name);
this.readme(oPlugin.readme);
+ this.author(oPlugin.author);
+ this.url(oPlugin.url);
+ this.version(oPlugin.version);
+ this.released(oPlugin.released);
const config = oPlugin.config;
if (arrayLength(config)) {
diff --git a/dev/View/Popup/SMimeImport.js b/dev/View/Popup/SMimeImport.js
index 7ebf721f9..c686c7570 100644
--- a/dev/View/Popup/SMimeImport.js
+++ b/dev/View/Popup/SMimeImport.js
@@ -29,8 +29,8 @@ export class SMimeImportPopupView extends AbstractViewPopup {
(iError, oData) => {
if (iError) {
this.pemError(true);
- this.pemErrorMessage(getNotification(iError, oData?.ErrorMessage));
-// oData?.ErrorMessageAdditional;
+ this.pemErrorMessage(getNotification(iError, oData?.message));
+// oData?.messageAdditional;
} else {
this.close();
}
diff --git a/dev/View/User/Login.js b/dev/View/User/Login.js
index 78f9b7bb0..f80262252 100644
--- a/dev/View/User/Login.js
+++ b/dev/View/User/Login.js
@@ -20,7 +20,6 @@ const
SignMeOn = 1,
SignMeUnused = 2;
-
export class LoginUserView extends AbstractViewLogin {
constructor() {
super();
@@ -119,9 +118,9 @@ export class LoginUserView extends AbstractViewLogin {
if (Notifications.InvalidInputArgument == iError) {
iError = Notifications.AuthError;
}
- this.submitError(getNotification(iError, oData?.ErrorMessage,
+ this.submitError(getNotification(iError, oData?.message,
Notifications.UnknownError));
- this.submitErrorAdditional(oData?.ErrorMessageAdditional);
+ this.submitErrorAdditional(oData?.messageAdditional || oData?.message);
} else {
rl.setData(oData.Result);
}
diff --git a/dev/View/User/MailBox/FolderList.js b/dev/View/User/MailBox/FolderList.js
index ef2ec7354..d8f60fe78 100644
--- a/dev/View/User/MailBox/FolderList.js
+++ b/dev/View/User/MailBox/FolderList.js
@@ -77,13 +77,14 @@ export class MailFolderList extends AbstractViewLeft {
const folder = ko.dataFor(el);
if (folder) {
if (moveAction()) {
- moveAction(false);
- let messages = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
+ const copy = event.ctrlKey || 2 === moveAction(),
+ messages = MessagelistUserStore.listCheckedOrSelectedUidsWithSubMails();
+ moveAction(0);
messages.size && MessagelistUserStore.moveMessages(
messages.folder,
messages,
folder.fullName,
- event.ctrlKey
+ copy
);
} else {
if (!SettingsUserStore.usePreviewPane()) {
@@ -160,7 +161,7 @@ export class MailFolderList extends AbstractViewLeft {
// addShortcut('tab', 'shift', ScopeFolderList, () => {
addShortcut('escape,tab,arrowright', '', ScopeFolderList, () => {
AppUserStore.focusedState(ScopeMessageList);
- moveAction(false);
+ moveAction(0);
return false;
});
}
diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js
index be5441b1b..e273e3069 100644
--- a/dev/View/User/MailBox/MessageList.js
+++ b/dev/View/User/MailBox/MessageList.js
@@ -1,6 +1,7 @@
import ko from 'ko';
import { addObservablesTo, addComputablesTo } from 'External/ko';
+import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { ScopeFolderList, ScopeMessageList, ScopeMessageView } from 'Common/Enums';
import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
import { doc,
@@ -110,6 +111,8 @@ export class MailMessageList extends AbstractViewRight {
this.userUsageProc = FolderUserStore.quotaPercentage;
+ this.hideDeleted = SettingsUserStore.hideDeleted;
+
addObservablesTo(this, {
focusSearch: false
});
@@ -162,11 +165,11 @@ export class MailMessageList extends AbstractViewRight {
let list = [], current, sort = FolderUserStore.sortMode() || 'DATE';
if (sort.includes('FROM')) {
MessagelistUserStore.forEach(msg => {
- let email = msg.from[0].email;
+ let email = msg.from[0]?.email;
if (!current || email != current.id) {
current = {
id: email,
- label: msg.from[0].toLine(),
+ label: msg.from[0]?.toLine(),
search: 'from=' + email,
messages: []
};
@@ -210,15 +213,19 @@ export class MailMessageList extends AbstractViewRight {
sortText: () => {
let mode = FolderUserStore.sortMode(),
- desc = '' === mode || mode.includes('REVERSE');
+ has = w => mode.includes(w),
+ desc = '' === mode || has('REVERSE');
mode = mode.split(/\s+/);
- if (mode.includes('FROM')) {
+ if (has('FROM')) {
return '@' + (desc ? '⬆' : '⬇');
}
- if (mode.includes('SUBJECT')) {
+ if (has('SUBJECT')) {
return '𝐒' + (desc ? '⬆' : '⬇');
}
- return (mode.includes('SIZE') ? '✉' : '📅') + (desc ? '⬇' : '⬆');
+ if (has('SIZE')) {
+ return '✉' + (desc ? '⬇' : '⬆');
+ }
+ return (has('ARRIVAL') ? '📨' : '📅') + (desc ? '⬇' : '⬆');
},
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
@@ -296,14 +303,14 @@ export class MailMessageList extends AbstractViewRight {
item => sFolder === item?.folder && iUid == item?.uid
);
- if ('INBOX' === sFolder) {
+ if ('INBOX' === sFolder && 'INBOX' !== FolderUserStore.currentFolderFullName()) {
hasher.setHash(mailBox(sFolder));
}
if (message) {
this.selector.selectMessageItem(message);
} else {
- if ('INBOX' !== sFolder) {
+ if ('INBOX' !== sFolder && sFolder !== FolderUserStore.currentFolderFullName()) {
hasher.setHash(mailBox(sFolder));
}
if (sFolder && iUid) {
@@ -326,11 +333,13 @@ export class MailMessageList extends AbstractViewRight {
downloadZipCommand: canBeMovedHelper,
forwardCommand: canBeMovedHelper,
deleteWithoutMoveCommand: canBeMovedHelper,
- deleteCommand: canBeMovedHelper,
+ deleteCommand: () => MessagelistUserStore.hasCheckedOrSelectedAndUndeleted(),
+ undeleteCommand: () => MessagelistUserStore.hasCheckedOrSelectedAndDeleted(),
archiveCommand: canBeMovedHelper,
spamCommand: canBeMovedHelper,
notSpamCommand: canBeMovedHelper,
moveCommand: canBeMovedHelper,
+ copyCommand: canBeMovedHelper
});
}
@@ -399,8 +408,30 @@ export class MailMessageList extends AbstractViewRight {
&& moveMessagesToFolderType(FolderType.Trash, true);
}
+ // User setting hideDeleted || immediatelyMoveToTrash ??
deleteCommand() {
- moveMessagesToFolderType(FolderType.Trash);
+ /**
+ * When FolderUserStore.trashFolder is set to "Do not use",
+ * flag as \Deleted for removal by later EXPUNGE
+ */
+ if (UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()) {
+ listAction(
+ FolderUserStore.currentFolderFullName(),
+ MessageSetAction.SetDeleted,
+ MessagelistUserStore.listCheckedOrSelected()
+ );
+ } else {
+ moveMessagesToFolderType(FolderType.Trash);
+ }
+ }
+
+ // User setting !hideDeleted && !immediatelyMoveToTrash ??
+ undeleteCommand() {
+ listAction(
+ FolderUserStore.currentFolderFullName(),
+ MessageSetAction.UnsetDeleted,
+ MessagelistUserStore.listCheckedOrSelected()
+ );
}
archiveCommand() {
@@ -415,18 +446,26 @@ export class MailMessageList extends AbstractViewRight {
moveMessagesToFolderType(FolderType.Inbox);
}
- moveCommand(vm, event) {
+ moveOrCopy(vm, event, mode) {
if (canBeMovedHelper()) {
if (vm && event?.preventDefault) {
stopEvent(event);
}
- let b = moveAction();
- AppUserStore.focusedState(b ? ScopeMessageList : ScopeFolderList);
- moveAction(!b);
+ let i = moveAction();
+ AppUserStore.focusedState(i ? ScopeMessageList : ScopeFolderList);
+ moveAction(i ? 0 : mode);
}
}
+ moveCommand(vm, event) {
+ this.moveOrCopy(vm, event, 1);
+ }
+
+ copyCommand(vm, event) {
+ this.moveOrCopy(vm, event, 2);
+ }
+
composeClick() {
showMessageComposer();
}
@@ -594,8 +633,10 @@ export class MailMessageList extends AbstractViewRight {
}
},
dblclick: event => {
- let el = eqs(event, '.messageListItem');
- el && this.gotoThread(ko.dataFor(el));
+ let msg = ko.dataFor(eqs(event, '.messageListItem'));
+ if (msg) {
+ msg.threadsLen() ? this.gotoThread(msg) : toggleFullscreen();
+ }
}
});
diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js
index bb31475fc..7139d9d03 100644
--- a/dev/View/User/MailBox/MessageView.js
+++ b/dev/View/User/MailBox/MessageView.js
@@ -1,6 +1,7 @@
import ko from 'ko';
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
+import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { ScopeFolderList, ScopeMessageList, ScopeMessageView } from 'Common/Enums';
import {
@@ -19,7 +20,8 @@ import {
fireEvent,
stopEvent,
addShortcut,
- registerShortcut
+ registerShortcut,
+ formFieldFocused
} from 'Common/Globals';
import { arrayLength } from 'Common/Utils';
@@ -54,9 +56,6 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
-import { IdentityUserStore } from 'Stores/User/Identity';
-
-import { Passphrases } from 'Storage/Passphrases';
const
oMessageScrollerDom = () => elementById('messageItem') || {},
@@ -99,6 +98,24 @@ export class MailMessageView extends AbstractViewRight {
currentMessage(null);
rl.app.moveMessagesToFolderType(folderType, message.folder, new Set([message.uid]), bDelete);
}
+ }, this.messageVisible),
+ createCommandSetHelper = action =>
+ createCommand(() => setAction(action), this.messageVisible),
+ createCommandDeleteHelper = () =>
+ createCommand(() => {
+ /**
+ * When FolderUserStore.trashFolder is set to "Do not use",
+ * flag as \Deleted for removal by later EXPUNGE
+ */
+ if (UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()) {
+ setAction(MessageSetAction.SetDeleted);
+ } else {
+ const message = currentMessage();
+ if (message) {
+ currentMessage(null);
+ rl.app.moveMessagesToFolderType(FolderType.Trash, message.folder, new Set([message.uid]));
+ }
+ }
}, this.messageVisible);
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
@@ -113,6 +130,8 @@ export class MailMessageView extends AbstractViewRight {
// viewer
viewFromShort: '',
dkimData: ['none', '', ''],
+ spfData: ['none', '', ''],
+ dmarcData: ['none', '', ''],
nowTracking: false
});
@@ -174,30 +193,62 @@ export class MailMessageView extends AbstractViewRight {
canBeRepliedOrForwarded: () => !MessagelistUserStore.isDraftFolder() && this.messageVisible(),
- viewDkimIcon: () => 'none' !== this.dkimData()[0],
-
- dkimIconClass:() => {
+ dkimIcon: () => {
switch (this.dkimData()[0]) {
case 'none':
- return '';
+ return '🚫︎';
case 'pass':
- return 'icon-ok iconcolor-green'; // ✔️
+ return '✔';
default:
- return 'icon-cross iconcolor-red'; // ✖ ❌
+ return '✖';
}
},
-
+ dkimIconClass: () => 'pass' === this.dkimData()[0] ? 'iconcolor-green' : 'iconcolor-red',
dkimTitle:() => {
const dkim = this.dkimData();
return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : '';
},
+ spfIcon: () => {
+ switch (this.spfData()[0]) {
+ case 'none':
+ return '🚫︎';
+ case 'pass':
+ return '✔';
+ default:
+ return '✖';
+ }
+ },
+ spfIconClass: () => 'pass' === this.spfData()[0] ? 'iconcolor-green' : 'iconcolor-red',
+ spfTitle:() => {
+ const spf = this.spfData();
+ return spf[0] ? spf[2] || 'SPF: ' + spf[0] : '';
+ },
+
+ dmarcIcon: () => {
+ switch (this.dmarcData()[0]) {
+ case 'none':
+ return '🚫︎';
+ case 'pass':
+ return '✔';
+ default:
+ return '✖';
+ }
+ },
+ dmarcIconClass: () => 'pass' === this.dmarcData()[0] ? 'iconcolor-green' : 'iconcolor-red',
+ dmarcTitle:() => {
+ const dmarc = this.dmarcData();
+ return dmarc[0] ? dmarc[2] || 'DMARC: ' + dmarc[0] : '';
+ },
+
showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(),
firstUnsubsribeLink: () => currentMessage()?.unsubsribeLinks()[0] || '',
pgpSupported: () => currentMessage() && PgpUserStore.isSupported(),
+ canBeUndeleted: () => currentMessage()?.isDeleted(),
+
messageListOrViewLoading:
() => MessagelistUserStore.isLoading() | MessageUserStore.loading()
});
@@ -212,6 +263,8 @@ export class MailMessageView extends AbstractViewRight {
// TODO: make first param a user setting #683
this.viewFromShort(message.from.toString(false, true));
this.dkimData(message.dkim[0] || ['none', '', '']);
+ this.spfData(message.spf[0] || ['none', '', '']);
+ this.dmarcData(message.dmarc[0] || ['none', '', '']);
this.nowTracking(false);
} else {
MessagelistUserStore.selectedMessage(null);
@@ -232,7 +285,11 @@ export class MailMessageView extends AbstractViewRight {
this.forwardAsAttachmentCommand = createCommandReplyHelper(ComposeType.ForwardAsAttachment);
this.editAsNewCommand = createCommandReplyHelper(ComposeType.EditAsNew);
- this.deleteCommand = createCommandActionHelper(FolderType.Trash);
+// this.deleteCommand = createCommandActionHelper(FolderType.Trash);
+ // User setting hideDeleted || immediatelyMoveToTrash ??
+ this.deleteCommand = createCommandDeleteHelper();
+ // User setting !hideDeleted && !immediatelyMoveToTrash ??
+ this.undeleteCommand = createCommandSetHelper(MessageSetAction.UnsetDeleted);
this.deleteWithoutMoveCommand = createCommandActionHelper(FolderType.Trash, true);
this.archiveCommand = createCommandActionHelper(FolderType.Archive);
this.spamCommand = createCommandActionHelper(FolderType.Junk);
@@ -241,6 +298,7 @@ export class MailMessageView extends AbstractViewRight {
decorateKoCommands(this, {
editCommand: self => self.messageVisible(),
moveCommand: self => self.messageVisible(),
+ copyCommand: self => self.messageVisible(),
goUpCommand: self => !self.messageListOrViewLoading(),
goDownCommand: self => !self.messageListOrViewLoading()
});
@@ -258,13 +316,21 @@ export class MailMessageView extends AbstractViewRight {
currentMessage() && showMessageComposer([ComposeType.Draft, currentMessage()]);
}
- moveCommand(vm, event) {
+ moveOrCopy(vm, event, mode) {
if (vm && event?.preventDefault) {
stopEvent(event);
}
this.actionsMenu().ddBtn.hide();
AppUserStore.focusedState(ScopeFolderList);
- moveAction(true);
+ moveAction(mode);
+ }
+
+ moveCommand(vm, event) {
+ this.moveOrCopy(vm, event, 1);
+ }
+
+ copyCommand(vm, event) {
+ this.moveOrCopy(vm, event, 2);
}
setUnseen() {
@@ -433,6 +499,13 @@ export class MailMessageView extends AbstractViewRight {
}
});
+ addShortcut('b', 'shift', [ScopeMessageList, ScopeMessageView], () => {
+ if (!formFieldFocused()) {
+ currentMessage()?.swapColors?.();
+ return false;
+ }
+ });
+
addShortcut('arrowup,arrowleft', 'meta', [ScopeMessageList, ScopeMessageView], () => {
this.goUpCommand();
return false;
@@ -541,20 +614,19 @@ export class MailMessageView extends AbstractViewRight {
* @returns {void}
*/
readReceipt() {
- let oMessage = currentMessage()
+ let oMessage = currentMessage();
if (oMessage.readReceipt) {
- Remote.request('SendReadReceiptMessage', iError => {
- if (!iError) {
- oMessage.flags.push('$mdnsent');
-// oMessage.flags.valueHasMutated();
+ oMessage.flags.push('$mdnsent');
+ Remote.request('SendReadReceiptMessage',
+ iError => iError && oMessage.flags.remove('$mdnsent'),
+ {
+ messageFolder: oMessage.folder,
+ messageUid: oMessage.uid,
+ readReceipt: oMessage.readReceipt,
+ subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
+ plain: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
}
- }, {
- messageFolder: oMessage.folder,
- messageUid: oMessage.uid,
- readReceipt: oMessage.readReceipt,
- subject: i18n('READ_RECEIPT/SUBJECT', { SUBJECT: oMessage.subject() }),
- plain: i18n('READ_RECEIPT/BODY', { 'READ-RECEIPT': AccountUserStore.email() })
- });
+ );
}
}
@@ -570,125 +642,19 @@ export class MailMessageView extends AbstractViewRight {
}
pgpDecrypt() {
- const oMessage = currentMessage(),
- data = oMessage.pgpEncrypted();
- delete data.error;
- PgpUserStore.decrypt(oMessage).then(result => {
- if (!result) {
- // TODO: translate
- throw Error('Decryption failed, canceled or not possible');
- }
- oMessage.pgpDecrypted(true);
- if (result.data) {
- MimeToMessage(result.data, oMessage);
- oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
- if (result.signatures?.length) {
- oMessage.pgpSigned({
- signatures: result.signatures,
- success: !!result.signatures.length
- });
- }
- }
- })
- .catch(e => {
- data.error = e.message;
- })
- .finally(() => {
- oMessage.pgpEncrypted(data);
- });
+ currentMessage().decrypt();
}
pgpVerify(/*self, event*/) {
- const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
- PgpUserStore.verify(oMessage).then(result => {
- if (result) {
- oMessage.pgpSigned(result);
- } else {
- alert('Verification failed or no valid public key found');
- }
-/*
- if (result?.success) {
- i18n('CRYPTO/GOOD_SIGNATURE', {
- USER: validKey.user + ' (' + validKey.id + ')'
- });
- message.getText()
- } else {
- const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
- additional = keyIds
- ? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
- : '';
-
- i18n('CRYPTO/ERROR', {
- TYPE: 'OpenPGP',
- ERROR: 'message'
- }) + (additional ? ' (' + additional + ')' : '');
- }
-*/
- });
+ currentMessage().pgpVerify();
}
async smimeDecrypt() {
- const message = currentMessage();
- const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
- identity = IdentityUserStore.find(item => addresses.includes(item.email())),
- data = message.smimeEncrypted(); // { partId: "1" }
- if (data && identity) {
- delete data.error;
- let pass, params = { ...data }; // clone
- params.folder = message.folder;
- params.uid = message.uid;
-// params.bodyPart = params.bodyPart?.raw;
- params.certificate = identity.smimeCertificate();
- params.privateKey = identity.smimeKey();
- if (identity.smimeKeyEncrypted()) {
- pass = await Passphrases.ask(identity,
- i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
- 'CRYPTO/DECRYPT'
- );
- if (!pass) {
- return;
- }
- params.passphrase = pass?.password;
- }
- Remote.post('SMimeDecryptMessage', null, params).then(response => {
- if (response?.Result?.data) {
- message.smimeDecrypted(true);
- MimeToMessage(response.Result.data, message);
- message.html() ? message.viewHtml() : message.viewPlain();
- pass && pass.remember && Passphrases.set(identity, pass.password);
- if ('signed' in response.Result) {
- message.smimeSigned(response.Result.signed);
- }
- }
- }).catch(e => {
- data.error = e.message
- })
- .finally(() => {
- message.smimeEncrypted(data);
- });
- }
+ currentMessage().decrypt();
}
smimeVerify(/*self, event*/) {
- const message = currentMessage(),
- data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
- if (data) {
- const params = { ...data }; // clone
- params.folder = message.folder;
- params.uid = message.uid;
- params.bodyPart = data.bodyPart?.raw;
- params.sigPart = data.sigPart?.bodyRaw;
- Remote.post('SMimeVerifyMessage', null, params).then(response => {
- if (response?.Result) {
- if (response.Result.body) {
- MimeToMessage(response.Result.body, message);
- message.html() ? message.viewHtml() : message.viewPlain();
- }
- data.success = response.Result.success;
- message.smimeSigned(data);
- }
- });
- }
+ currentMessage().smimeVerify();
}
}
diff --git a/dev/View/User/SystemDropDown.js b/dev/View/User/SystemDropDown.js
index 1b10b4fcd..34a7880a6 100644
--- a/dev/View/User/SystemDropDown.js
+++ b/dev/View/User/SystemDropDown.js
@@ -59,7 +59,7 @@ export class SystemDropDownUserView extends AbstractViewRight {
(iError/*, oData*/) => {
if (iError) {
AccountUserStore.loading(false);
- alert(getNotification(iError).replace('%EMAIL%', email));
+ alert('Account error: ' + getNotification(iError).replace('%EMAIL%', email));
if (account.isAdditional()) {
showScreenPopup(AccountPopupView, [account]);
}
diff --git a/dev/boot.js b/dev/boot.js
index eed775739..f8c9ac381 100644
--- a/dev/boot.js
+++ b/dev/boot.js
@@ -4,6 +4,7 @@ const
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
eId = id => doc.getElementById('rl-'+id),
admin = '1' == eId('app').dataset.admin,
+ mimeJSON = 'application/json',
toggle = div => {
eId('loading').hidden = true;
@@ -92,7 +93,7 @@ window.rl = {
}
}
if (asJSON) {
- init.headers['Content-Type'] = 'application/json';
+ init.headers['Content-Type'] = mimeJSON;
postData = JSON.stringify(postData);
}
init.body = postData;
@@ -104,9 +105,13 @@ window.rl = {
fetchJSON: (resource, init, postData) => {
init = Object.assign({ headers: {} }, init);
- init.headers.Accept = 'application/json';
+ init.headers.Accept = mimeJSON;
return rl.fetch(resource, init, postData).then(response => {
if (response.ok) {
+ const ct = response.headers.get('Content-Type');
+ if (!ct.startsWith(mimeJSON)) {
+ return Promise.reject(new Error(`Invalid Content-Type '${ct}' for url '${resource}'`));
+ }
/* TODO: use this for non-developers?
response.clone()
let data = response.text();
@@ -118,15 +123,15 @@ window.rl = {
return Promise.reject(Notifications.JsonParse);
return {
Result: false,
- ErrorCode: 952, // Notifications.JsonParse
- ErrorMessage: e.message,
- ErrorMessageAdditional: data
+ code: 952, // Notifications.JsonParse
+ message: e.message,
+ messageAdditional: data
}
}
*/
return response.json();
}
- return Promise.reject('Network response error: ' + response.status);
+ return Promise.reject(new Error('Network response error: ' + response.status));
});
}
};
diff --git a/dev/dragdropgecko.js b/dev/dragdropgecko.js
index 65836b5e9..b87115cca 100644
--- a/dev/dragdropgecko.js
+++ b/dev/dragdropgecko.js
@@ -75,8 +75,8 @@
// create drag image from custom element or drag source
img = src.cloneNode(true);
copyStyle(src, img);
- img._x = xOffset == null ? src.clientWidth / 2 : xOffset;
- img._y = yOffset == null ? src.clientHeight / 2 : yOffset;
+ img._x = xOffset ?? src.clientWidth / 2;
+ img._y = yOffset ?? src.clientHeight / 2;
}
},
diff --git a/dev/sieve.js b/dev/sieve.js
index 33a8dec9e..122eb2e6c 100644
--- a/dev/sieve.js
+++ b/dev/sieve.js
@@ -57,7 +57,7 @@ window.Sieve = {
Remote.request('FiltersScriptDelete',
(iError, data) =>
iError
- ? setError(data?.ErrorMessageAdditional || getNotification(iError))
+ ? setError(data?.messageAdditional || getNotification(iError))
: scripts.remove(script)
,
{name:script.name()}
@@ -69,7 +69,7 @@ window.Sieve = {
Remote.request('FiltersScriptActivate',
(iError, data) =>
iError
- ? setError(data?.ErrorMessageAdditional || iError)
+ ? setError(data?.messageAdditional || iError)
: scripts.forEach(script => script.active(script.name() === name))
,
{name:name}
diff --git a/examples/docker/Docker.md b/examples/docker/Docker.md
index 68b5baa57..9d5ec834f 100644
--- a/examples/docker/Docker.md
+++ b/examples/docker/Docker.md
@@ -47,12 +47,12 @@ To setup MySQL as the DB, in Admin Panel, click `Contacts`, check `Enable contac
Click the `Test` button. If it turns green, MySQL is ready to be used for contacts.
-To setup Redis for caching, in Admin Panel, click `Config`, update the following configuration options:
+To setup Redis for caching, in Admin Panel, click `Extensions`, update the following configuration options:
-- `cache > enable`: yes
-- `cache > fast_cache_driver`: `redis`
-- `labs > fast_cache_redis_host`: `redis`
-- `labs > fast_cache_redis_port`: `6379`
+- `Cache Redis`: `install`
+- `Cache Redis`: `enable`
+- `Cache Redis > host`: `redis`
+- `Cache Redis > port`: `6379`
Redis caching is now enabled.
@@ -77,18 +77,18 @@ Now, login to [http://localhost:8888/?admin](http://localhost:8888/?admin) with
To use PostgreSQL as the DB, in Admin Panel, click `Contacts`, check `Enable contacts` and , and under `Storage (PDO)` choose the following:
- Type: `PostgresSQL`
-- Data Source Name (DSN): `host=postgres;port=5432;dbname=snappymail`
+- Data Source Name (DSN): `host=db;port=5432;dbname=snappymail`
- User `snappymail`
- Password `snappymail`
Click the `Test` button. If it turns green, PostgreSQL is ready to be used for contacts.
-To setup Redis for caching, in Admin Panel, click `Config`, update the following configuration options:
+To setup Redis for caching, in Admin Panel, click `Extensions`, update the following configuration options:
-- `cache > enable`: yes
-- `cache > fast_cache_driver`: `redis`
-- `labs > fast_cache_redis_host`: `redis`
-- `labs > fast_cache_redis_port`: `6379`
+- `Cache Redis`: `install`
+- `Cache Redis`: `enable`
+- `Cache Redis > host`: `redis`
+- `Cache Redis > port`: `6379`
Redis caching is now enabled.
diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md
index 54ddaca71..c3a50a8fa 100644
--- a/integrations/cloudron/DESCRIPTION.md
+++ b/integrations/cloudron/DESCRIPTION.md
@@ -1,4 +1,4 @@
-This app packages SnappyMail 2.35.4.
+This app packages SnappyMail 2.38.2.
SnappyMail is a simple, modern, lightweight & fast web-based email client.
diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile
index 15b49d1cb..a3d442e96 100644
--- a/integrations/cloudron/Dockerfile
+++ b/integrations/cloudron/Dockerfile
@@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
-VERSION=2.35.4
+VERSION=2.38.2
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \
diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml
index 560564654..d85ad52ed 100644
--- a/integrations/nextcloud/snappymail/appinfo/info.xml
+++ b/integrations/nextcloud/snappymail/appinfo/info.xml
@@ -3,7 +3,7 @@
snappymail
SnappyMail
SnappyMail Webmail
- 2.35.4
+ 2.38.2
agpl
SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli
uuid
xxtea
zip
+
-->
-
+
OCA\SnappyMail\Settings\AdminSettings
@@ -86,4 +87,7 @@ There, click on the link to go to the SnappyMail admin panel.
OCA\SnappyMail\Migration\InstallStep
+
+ OCA\SnappyMail\Command\Settings
+
diff --git a/integrations/nextcloud/snappymail/l10n/eu.json b/integrations/nextcloud/snappymail/l10n/eu.json
index d981fe513..d9ef0f7e4 100644
--- a/integrations/nextcloud/snappymail/l10n/eu.json
+++ b/integrations/nextcloud/snappymail/l10n/eu.json
@@ -1,5 +1,5 @@
{ "translations": {
- "Email" : "Helbide elektronikoa",
+ "Email" : "Posta",
"Error" : "Errorea",
"Invalid argument(s)" : "Argumentu baliogabea(k)",
"Saved successfully" : "Ondo gorde da",
@@ -16,4 +16,4 @@
"Enter an email and password to auto-login to SnappyMail." : "Saioa automatikoki hasteko sartu helbide elektronikoa eta pasahitza SnappyMail-en.",
"Password" : "Pasahitza"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
-}
\ No newline at end of file
+}
diff --git a/integrations/nextcloud/snappymail/lib/AppInfo/Application.php b/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
index df40033cf..d764520ca 100644
--- a/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
+++ b/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
@@ -7,6 +7,7 @@ use OCA\SnappyMail\Controller\FetchController;
use OCA\SnappyMail\Controller\PageController;
use OCA\SnappyMail\Dashboard\UnreadMailWidget;
use OCA\SnappyMail\Search\Provider;
+use OCA\SnappyMail\Listeners\AccessTokenUpdatedListener;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -16,6 +17,7 @@ use OCP\IL10N;
use OCP\IUser;
use OCP\User\Events\PostLoginEvent;
use OCP\User\Events\BeforeUserLoggedOutEvent;
+use OCA\OIDCLogin\Events\AccessTokenUpdatedEvent;
class Application extends App implements IBootstrap
{
@@ -62,6 +64,7 @@ class Application extends App implements IBootstrap
);
$context->registerSearchProvider(Provider::class);
+ $context->registerEventListener(AccessTokenUpdatedEvent::class, AccessTokenUpdatedListener::class);
// TODO: Not working yet, needs a Vue UI
// $context->registerDashboardWidget(UnreadMailWidget::class);
diff --git a/integrations/nextcloud/snappymail/lib/Command/Settings.php b/integrations/nextcloud/snappymail/lib/Command/Settings.php
new file mode 100644
index 000000000..1e6cae25d
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Command/Settings.php
@@ -0,0 +1,70 @@
+userManager = $userManager;
+ $this->config = $config;
+ }
+
+ protected function configure() {
+ $this
+ ->setName('snappymail:settings')
+ ->setDescription('modifies configuration')
+ ->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User ID used to login'
+ )
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'The login username'
+ )
+ ->addArgument(
+ 'pass',
+ InputArgument::OPTIONAL,
+ 'The login passphrase'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $uid = $input->getArgument('uid');
+ if (!$this->userManager->userExists($uid)) {
+ $output->writeln('The user "' . $uid . '" does not exist.');
+ return 1;
+ }
+
+ $sEmail = $input->getArgument('user');
+ $this->config->setUserValue($uid, 'snappymail', 'snappymail-email', $sEmail);
+
+ $sPass = $input->getArgument('pass');
+ if (empty($sPass)) {
+ // Prompt on command line for value
+ if (\is_callable('readline')) {
+ $sPass = \readline("password: ");
+ } else {
+ echo "password: ";
+ $sPass = \stream_get_line(STDIN, 1024, PHP_EOL);
+ }
+ }
+ $sPass = ($sEmail && $sPass) ? SnappyMailHelper::encodePassword($sPass, \md5($sEmail)) : '';
+ $this->config->setUserValue($uid, 'snappymail', 'passphrase', $sPass);
+ return 0;
+ }
+}
diff --git a/integrations/nextcloud/snappymail/lib/Controller/FetchController.php b/integrations/nextcloud/snappymail/lib/Controller/FetchController.php
index 0baa2a4df..6ba8a7761 100644
--- a/integrations/nextcloud/snappymail/lib/Controller/FetchController.php
+++ b/integrations/nextcloud/snappymail/lib/Controller/FetchController.php
@@ -53,8 +53,7 @@ class FetchController extends Controller {
$this->config->setAppValue('snappymail', 'snappymail-autologin-with-email',
isset($_POST['snappymail-autologin']) ? '2' === $_POST['snappymail-autologin'] : false);
$this->config->setAppValue('snappymail', 'snappymail-no-embed', isset($_POST['snappymail-no-embed']));
- // DISABLED https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
-// $this->config->setAppValue('snappymail', 'snappymail-autologin-oidc', isset($_POST['snappymail-autologin-oidc']));
+ $this->config->setAppValue('snappymail', 'snappymail-autologin-oidc', isset($_POST['snappymail-autologin-oidc']));
} else {
return new JSONResponse([
'status' => 'error',
@@ -62,6 +61,16 @@ class FetchController extends Controller {
]);
}
+ SnappyMailHelper::loadApp();
+
+ $oConfig = \RainLoop\Api::Config();
+ if (!empty($_POST['snappymail-app_path'])) {
+ $oConfig->Set('webmail', 'app_path', $_POST['snappymail-app_path']);
+ }
+ $oConfig->Set('webmail', 'allow_languages_on_settings', empty($_POST['snappymail-nc-lang']));
+ $oConfig->Set('login', 'allow_languages_on_login', empty($_POST['snappymail-nc-lang']));
+ $oConfig->Save();
+
if (!empty($_POST['import-rainloop'])) {
return new JSONResponse([
'status' => 'success',
@@ -69,7 +78,6 @@ class FetchController extends Controller {
]);
}
- SnappyMailHelper::loadApp();
$debug = !empty($_POST['snappymail-debug']);
$oConfig = \RainLoop\Api::Config();
if ($debug != $oConfig->Get('debug', 'enable', false)) {
@@ -114,12 +122,21 @@ class FetchController extends Controller {
]);
}
+ // Logout as the credentials have changed
+ SnappyMailHelper::loadApp();
+ \RainLoop\Api::Actions()->DoLogout();
+
return new JSONResponse([
'status' => 'success',
'Message' => $this->l->t('Saved successfully'),
'Email' => $sEmail
]);
} catch (Exception $e) {
+ // Logout as the credentials might have changed, as exception could be in one attribute
+ // TODO: Handle both exceptions separately?
+ SnappyMailHelper::loadApp();
+ \RainLoop\Api::Actions()->DoLogout();
+
return new JSONResponse([
'status' => 'error',
'Message' => $e->getMessage()
diff --git a/integrations/nextcloud/snappymail/lib/Listeners/AccessTokenUpdatedListener.php b/integrations/nextcloud/snappymail/lib/Listeners/AccessTokenUpdatedListener.php
new file mode 100644
index 000000000..7b94359a7
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Listeners/AccessTokenUpdatedListener.php
@@ -0,0 +1,45 @@
+userSession = $userSession;
+ $this->session = $session;
+ $this->appManager = $appManager;
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof AccessTokenUpdatedEvent) || !$this->userSession->isLoggedIn() || !$this->session->exists('is_oidc')) {
+ return;
+ }
+ // just-in-case checks(also maybe useful for selfhosters)
+ if (!$this->appManager->isEnabledForUser(self::SNAPPYMAIL_APP_ID) || !$this->appManager->isEnabledForUser(self::OIDC_LOGIN_APP_ID)) {
+ return;
+ }
+ $accessToken = $event->getAccessToken();
+ if (!$accessToken) {
+ return;
+ }
+
+ $username = $this->userSession->getUser()->getUID();
+ $this->session->set('snappymail-nc-uid', $username);
+ }
+}
diff --git a/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php b/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php
index cca015aa9..966ef2352 100644
--- a/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php
+++ b/integrations/nextcloud/snappymail/lib/Migration/InstallStep.php
@@ -54,6 +54,8 @@ class InstallStep implements IRepairStep
if (!$oConfig->Get('webmail', 'app_path')) {
$output->info('Set config [webmail]app_path');
$oConfig->Set('webmail', 'app_path', \OC::$server->getAppManager()->getAppWebPath('snappymail') . '/app/');
+ $oConfig->Set('webmail', 'allow_languages_on_settings', false);
+ $oConfig->Set('login', 'allow_languages_on_login', false);
$bSave = true;
}
diff --git a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
index 4dca45612..7a69cb96f 100644
--- a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
+++ b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
@@ -22,9 +22,8 @@ class AdminSettings implements ISettings
$keys = [
'snappymail-autologin',
'snappymail-autologin-with-email',
- 'snappymail-no-embed'
- // DISABLED https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
-// 'snappymail-autologin-oidc'
+ 'snappymail-no-embed',
+ 'snappymail-autologin-oidc'
];
$parameters = [];
foreach ($keys as $k) {
@@ -63,6 +62,17 @@ class AdminSettings implements ISettings
}
}
+ // Prevent "Failed loading /nextcloud/snappymail/v/2.N.N/static/js/min/libs.min.js"
+ $app_path = $oConfig->Get('webmail', 'app_path');
+ if (!$app_path) {
+ $app_path = \OC::$server->getAppManager()->getAppWebPath('snappymail') . '/app/';
+ $oConfig->Set('webmail', 'app_path', $app_path);
+ $oConfig->Set('webmail', 'theme', 'NextcloudV25+');
+ $oConfig->Save();
+ }
+ $parameters['snappymail-app_path'] = $oConfig->Get('webmail', 'app_path', false);
+ $parameters['snappymail-nc-lang'] = !$oConfig->Get('webmail', 'allow_languages_on_settings', true);
+
\OCP\Util::addScript('snappymail', 'snappymail');
return new TemplateResponse('snappymail', 'admin-local', $parameters);
}
diff --git a/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php b/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
index dcfcd1453..801bf09aa 100644
--- a/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
+++ b/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
@@ -18,18 +18,6 @@ class PersonalSettings implements ISettings
{
$uid = \OC::$server->getUserSession()->getUser()->getUID();
$sEmail = $this->config->getUserValue($uid, 'snappymail', 'snappymail-email');
- if (!$sEmail) {
- $aRainLoop = \OCA\SnappyMail\Util\RainLoop::getLoginCredentials($uid, $this->config);
- if ($aRainLoop) {
- $sEmail = $aRainLoop[0];
- $this->config->setUserValue($uid, 'snappymail', 'snappymail-email', $sEmail);
- if ($aRainLoop[1]) {
- $this->config->setUserValue($uid, 'snappymail', 'passphrase',
- \OCA\SnappyMail\Util\SnappyMailHelper::encodePassword($aRainLoop[1], \md5($sEmail))
- );
- }
- }
- }
if ($sPass = $this->config->getUserValue($uid, 'snappymail', 'snappymail-password')) {
$this->config->deleteUserValue($uid, 'snappymail', 'snappymail-password');
$this->config->setUserValue($uid, 'snappymail', 'passphrase', $sPass);
diff --git a/integrations/nextcloud/snappymail/lib/Util/RainLoop.php b/integrations/nextcloud/snappymail/lib/Util/RainLoop.php
index e49e9dc1c..7f8073aa2 100644
--- a/integrations/nextcloud/snappymail/lib/Util/RainLoop.php
+++ b/integrations/nextcloud/snappymail/lib/Util/RainLoop.php
@@ -4,20 +4,6 @@ namespace OCA\SnappyMail\Util;
class RainLoop
{
-
- public static function getLoginCredentials($sUID, $config) : ?array
- {
- $sEmail = \trim($config->getUserValue($sUID, 'rainloop', 'rainloop-email'));
- if ($sEmail) {
- $sPassword = $config->getUserValue($sUID, 'rainloop', 'rainloop-password', '');
- return [
- $sEmail,
- $sPassword ? static::decodePassword($sPassword, \md5($sEmail)) : ''
- ];
- }
- return null;
- }
-
/**
* Imports data from RainLoop
* skips: /data/rainloop-storage/_data_/_default_/configs/application.ini
@@ -82,47 +68,4 @@ class RainLoop
return $result;
}
-
- /**
- * @param string $sPassword
- * @param string $sSalt
- *
- * @return string
- */
- public static function decodePassword($sPassword, $sSalt)
- {
- $sPassword = \base64_decode(\trim($sPassword));
-
- $method = 'AES-256-CBC';
- if (\function_exists('openssl_encrypt')
- && \function_exists('openssl_decrypt')
- && \function_exists('openssl_random_pseudo_bytes')
- && \function_exists('openssl_cipher_iv_length')
- && \function_exists('openssl_get_cipher_methods')
- && \defined('OPENSSL_RAW_DATA')
- && \defined('OPENSSL_ZERO_PADDING')
- && \in_array($method, \openssl_get_cipher_methods())
- ) {
- $aParts = \explode('|', $sPassword, 2);
- if (\is_array($aParts) && !empty($aParts[0]) && !empty($aParts[1])) {
- $sData = \base64_decode($aParts[0]);
- $iv = \base64_decode($aParts[1]);
- return \base64_decode(\trim(
- \openssl_decrypt($sData, $method, \md5($sSalt), OPENSSL_RAW_DATA, $iv)
- ));
- }
- }
-
- if (\function_exists('mcrypt_encrypt')
- && \function_exists('mcrypt_decrypt')
- && \defined('MCRYPT_RIJNDAEL_256')
- && \defined('MCRYPT_MODE_ECB')
- ) {
- return \base64_decode(\trim(
- \mcrypt_decrypt(MCRYPT_RIJNDAEL_256, \md5($sSalt), $sPassword, MCRYPT_MODE_ECB)
- ));
- }
-
- return $sPassword;
- }
}
diff --git a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
index 7d6016747..deb7fb465 100644
--- a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
+++ b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
@@ -90,24 +90,23 @@ class SnappyMailHelper
}
*/
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
+ $isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|');
try {
+ $ocSession = \OC::$server->getSession();
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
- if ($oAccount) {
- // Must be here due to bug #1241
- $oActions->SetMainAuthAccount($oAccount);
- $oActions->Plugins()->RunHook('login.success', array($oAccount));
-
- $oActions->SetAuthToken($oAccount);
- if ($oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) {
- $oActions->SetSignMeToken($oAccount);
- }
+ if (!$isOIDC && $oAccount
+ && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn
+ ) {
+ $oActions->SetSignMeToken($oAccount);
}
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
- $sUID = \OC::$server->getUserSession()->getUser()->getUID();
- \OC::$server->getSession()['snappymail-passphrase'] = '';
- \OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
- \SnappyMail\Log::error('Nextcloud', $e->getMessage());
+ if (!$isOIDC) {
+ $sUID = \OC::$server->getUserSession()->getUser()->getUID();
+ \OC::$server->getSession()['snappymail-passphrase'] = '';
+ \OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
+ \SnappyMail\Log::error('Nextcloud', $e->getMessage());
+ }
}
}
}
@@ -124,6 +123,32 @@ class SnappyMailHelper
}
}
+ // Check if OpenID Connect (OIDC) is enabled and used for login
+ // https://apps.nextcloud.com/apps/oidc_login
+ public static function isOIDCLogin() : bool
+ {
+ $config = \OC::$server->getConfig();
+ if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
+ // Check if the OIDC Login app is enabled
+ if (\OC::$server->getAppManager()->isEnabledForUser('oidc_login')) {
+ // Check if session is an OIDC Login
+ $ocSession = \OC::$server->getSession();
+ if ($ocSession->get('is_oidc')) {
+ // IToken->getPassword() ???
+ if ($ocSession->get('oidc_access_token')) {
+ return true;
+ }
+ \SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
+ } else {
+ \SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
+ }
+ } else {
+ \SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled');
+ }
+ }
+ return false;
+ }
+
private static function getLoginCredentials() : array
{
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
@@ -147,22 +172,14 @@ class SnappyMailHelper
// If the current user ID is identical to login ID (not valid when using account switching),
// this has the second priority.
if ($ocSession['snappymail-nc-uid'] == $sUID) {
-/*
+
// If OpenID Connect (OIDC) is enabled and used for login, use this.
- // https://apps.nextcloud.com/apps/oidc_login
- // DISABLED https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
- if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
- if ($ocSession->get('is_oidc')) {
- // IToken->getPassword() ???
- if ($sAccessToken = $ocSession->get('oidc_access_token')) {
- return [$sUID, 'oidc@nextcloud', $sAccessToken];
- }
- \SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
- } else {
- \SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
- }
+ if (static::isOIDCLogin()) {
+ $sEmail = $config->getUserValue($sUID, 'settings', 'email');
+ $pwd = new \SnappyMail\SensitiveString("oidc_login|{$sUID}");
+ return [$sUID, $sEmail, $pwd];
}
-*/
+
// Only use the user's password in the current session if they have
// enabled auto-login using Nextcloud username or email address.
$sEmail = '';
diff --git a/integrations/nextcloud/snappymail/templates/admin-local.php b/integrations/nextcloud/snappymail/templates/admin-local.php
index 91cfd5531..5270f3e50 100644
--- a/integrations/nextcloud/snappymail/templates/admin-local.php
+++ b/integrations/nextcloud/snappymail/templates/admin-local.php
@@ -38,15 +38,15 @@
-
+
>
+
+
+ >
+
+
+
+
+
+ style="width:20em">
+
+
+
diff --git a/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php
index 04ac12bc1..8835da06c 100644
--- a/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php
+++ b/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php
@@ -142,10 +142,6 @@ class SnappyMailHelper
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
$oActions->Logger()->AddSecret($aCredentials[2]);
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
- if ($oAccount) {
- $oActions->Plugins()->RunHook('login.success', array($oAccount));
- $oActions->SetAuthToken($oAccount);
- }
}
}
} catch (\Throwable $e) {
diff --git a/integrations/virtualmin/snappymail.pl b/integrations/virtualmin/snappymail.pl
index 58b78688b..c3fcc7ab5 100644
--- a/integrations/virtualmin/snappymail.pl
+++ b/integrations/virtualmin/snappymail.pl
@@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions()
sub script_snappymail_versions
{
-return ( "2.35.4" );
+return ( "2.38.2" );
}
sub script_snappymail_version_desc
diff --git a/package.json b/package.json
index 0dee6725c..3e7ef29eb 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
- "version": "2.35.4",
+ "version": "2.38.2",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",
@@ -43,10 +43,12 @@
"dependencies": {
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
+ "@rollup/plugin-terser": "^1.0.2",
"babel-eslint": "^10.1.0",
"del": "^6.0.0",
"eslint": "^7.32.0",
- "gulp": "^4.0.2",
+ "gulp": "^5.0.1",
+ "gulp-append-prepend": "^1.0.9",
"gulp-cached": "^1.1.1",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
@@ -55,7 +57,6 @@
"gulp-expect-file": "^2.0.0",
"gulp-filter": "^6.0.0",
"gulp-group-css-media-queries": "^1.2.2",
- "gulp-header": "^2.0.9",
"gulp-less": "^5.0.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.1.3",
@@ -64,8 +65,6 @@
"gulp-terser": "^2.1.0",
"rollup": "^2.56.3",
"rollup-plugin-external-globals": "^0.6.1",
- "rollup-plugin-html": "^0.2.1",
- "rollup-plugin-includepaths": "^0.2.4",
- "rollup-plugin-terser": "^7.0.2"
+ "rollup-plugin-includepaths": "^0.2.4"
}
}
diff --git a/plugins/README.md b/plugins/README.md
index b9b38cf59..7e318c186 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -124,16 +124,30 @@ $Plugin->addHook('hook.name', 'functionName');
params:
string &$sEmail
+ Happens in resolveLoginCredentials($sEmail) BEFORE resolving domain name.
+ This is the pure text from the login screen (DoLogin) or the SSO feature (ServiceSso) received by LoginProcess().
+ - DoLogin() -> LoginProcess() -> resolveLoginCredentials($sEmail)
+ - ServiceSso() -> LoginProcess() -> resolveLoginCredentials($sEmail)
+ So $sEmail can just have the value `test` without a domain.
+
### login.credentials.step-2
params:
string &$sEmail
string &$sPassword
+ Happens in resolveLoginCredentials($sEmail) AFTER resolving domain name.
+ So $sEmail always has a domain (for example `test` is now `test@example.com`).
+
### login.credentials
params:
string &$sEmail
- string &$sLogin
+ string &$sImapUser
string &$sPassword
+ string &$sSmtpUser
+
+ $sEmail is the domain imap->fixUsername() without shortening.
+ $sImapUser is the domain imap->fixUsername() for login into IMAP.
+ $sSmtpUser is the domain smtp->fixUsername() for login into SMTP.
### login.success
params:
@@ -411,12 +425,6 @@ and called in JavaScript using rl.pluginRemoteRequest().
string $sName
mixed &$mResult
-### service.app-delay-start-begin
- no params
-
-### service.app-delay-start-end
- no params
-
# JavaScript Events
## mailbox
@@ -454,12 +462,17 @@ and called in JavaScript using rl.pluginRemoteRequest().
### rl-view-model.create
event.detail = the ViewModel class
- Happens immediately after the ViewModel constructor
+ Happens immediately after the ViewModel constructor.
+ See accessible properties as https://github.com/the-djmaze/snappymail/blob/master/dev/Knoin/AbstractViews.js
### rl-view-model
event.detail = the ViewModel class
Happens after the full build (vm.onBuild()) and contains viewModelDom
+### rl-vm-visible
+ event.detail = the ViewModel class
+ Happens after the model is made visible (vm.afterShow())
+
### sm-admin-login
event.detail = FormData
cancelable using preventDefault()
diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js
index 3c252e848..0c09bcbd6 100644
--- a/plugins/avatars/avatars.js
+++ b/plugins/avatars/avatars.js
@@ -38,7 +38,10 @@
queue = [],
avatars = new Map,
ncAvatars = new Map,
+ identicons = new Map,
templateId = 'MailMessageView',
+ b64 = data => btoa(unescape(encodeURIComponent(data))),
+ b64url = data => b64(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
getBimiSelector = msg => {
// Get 's' value out of 'v=BIMI1; s=foo;'
let bimiSelector = msg.headers().valueByName('BIMI-Selector');
@@ -46,9 +49,21 @@
return bimiSelector || '';
},
getBimiId = msg => ('pass' == msg.from[0].dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg),
- getAvatarUrl = msg => `?Avatar/${getBimiId(msg)}/${msg.avatar}`,
- getAvatarUid = msg => `${getBimiId(msg)}/${msg.from[0].email.toLowerCase()}`,
- getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)),
+ email = msg => msg.from[0].email.toLowerCase(),
+ getAvatarUid = msg => `${getBimiId(msg)}/${email(msg)}`,
+ getAvatar = msg => ncAvatars.get(email(msg)) || avatars.get(getAvatarUid(msg)),
+ getAvatarUri = msg => {
+ if ('remote' === msg.avatar) {
+ msg.avatar = `?Avatar/${getBimiId(msg)}/${b64url(email(msg))}`;
+ }
+/*
+ else if (!msg.avatar.startsWith('data:')) {
+ msg.avatar = null;
+ }
+*/
+ return msg.avatar;
+ },
+
hash = async txt => {
if (/^[0-9a-f]{15,}$/i.test(txt)) {
return txt;
@@ -64,124 +79,141 @@
(from.name?.split(/[^\p{L}]+/gu) || []).reduce((a, s) => a + (s[0] || ''), '')
.slice(0,2)
.toUpperCase(),
- setIdenticon = (from, fn) => hash(from.email).then(hash =>
- fn('data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(window.identiconSvg(
- hash,
- fromChars(from),
- window.getComputedStyle(getEl('rl-app'), null).getPropertyValue('font-family')
- )))))
- ),
+ setIdenticon = (msg, fn, cb) => {
+ const from = email(msg);
+ if (identicons.get(from)) {
+ fn(identicons.get(from));
+ } else {
+ hash(from).then(hash => {
+ const uri = 'data:image/svg+xml;base64,' + b64(window.identiconSvg(
+ hash,
+ fromChars(msg.from[0]),
+ window.getComputedStyle(getEl('rl-app'), null).getPropertyValue('font-family')
+ ));
+ fn(uri);
+ identicons.set(email(msg), uri);
+ cb?.(uri);
+ });
+ }
+ },
+
addQueue = (msg, fn) => {
- msg.from?.[0] && setIdenticon(msg.from[0], fn);
- if (rl.pluginSettingsGet('avatars', 'delay')) {
- queue.push([msg, fn]);
- runQueue();
+ if (msg.from?.[0]) {
+ if (getAvatarUri(msg)) {
+ if (rl.pluginSettingsGet('avatars', 'delay')) {
+ setIdenticon(msg, fn, ()=>{
+ queue.push([msg, fn]);
+ runQueue();
+ });
+ } else {
+ fn(msg.avatar);
+ }
+ } else {
+ setIdenticon(msg, fn);
+ }
}
},
runQueue = (() => {
let item = queue.shift();
while (item) {
- if (item[0].from) {
- let url = getAvatar(item[0]),
- uid = getAvatarUid(item[0]);
- if (url) {
- item[1](url);
- item = queue.shift();
- continue;
- } else if (!avatars.has(uid)) {
- let from = item[0].from[0];
- rl.pluginRemoteRequest((iError, data) => {
- if (!iError && data?.Result.type) {
- url = `data:${data.Result.type};base64,${data.Result.data}`;
- avatars.set(uid, url);
- item[1](url);
- } else {
- avatars.set(uid, '');
- }
- runQueue();
- }, 'Avatar', {
- bimi: 'pass' == from.dkimStatus ? 1 : 0,
- bimiSelector: getBimiSelector(item[0]),
- email: from.email
- });
- break;
- }
+ let url = getAvatar(item[0]);
+ if (url) {
+ item[1](url);
+ item = queue.shift();
+ continue;
+ } else if (item[0].avatar) {
+ item[1](item[0].avatar);
}
runQueue();
break;
}
}).debounce(1000);
- /**
- * Loads images from Nextcloud contacts
- */
- addEventListener('DOMContentLoaded', () => {
+ /**
+ * Modify templates
+ */
+ getEl('MailMessageList').content.querySelectorAll('.messageCheckbox')
+ .forEach(el => el.append(Element.fromHTML(`
`)));
+ const messageItemHeader = getEl(templateId).content.querySelector('.messageItemHeader');
+ if (messageItemHeader) {
+ messageItemHeader.prepend(Element.fromHTML(
+ `
`
+ ));
+ }
+
+ /**
+ * Loads images from Nextcloud contacts
+ */
// rl.pluginSettingsGet('avatars', 'nextcloud');
- if (parent.OC) {
- const OC = () => parent.OC,
+ if (parent.OC?.requestToken) {
+ const OC = parent.OC,
nsDAV = 'DAV:',
nsNC = 'http://nextcloud.com/ns',
nsCard = 'urn:ietf:params:xml:ns:carddav',
getElementsByTagName = (parent, namespace, localName) => parent.getElementsByTagNameNS(namespace, localName),
getElementValue = (parent, namespace, localName) =>
- getElementsByTagName(parent, namespace, localName)?.item(0)?.textContent,
- generateUrl = path => OC().webroot + '/remote.php' + path;
- if (OC().requestToken) {
- fetch(generateUrl(`/dav/addressbooks/users/${OC().currentUser}/contacts/`), {
- mode: 'same-origin',
- cache: 'no-cache',
- redirect: 'error',
- credentials: 'same-origin',
- method: 'REPORT',
- headers: {
- requesttoken: OC().requestToken,
- 'Content-Type': 'application/xml; charset=utf-8',
- Depth: 1
- },
- body: ''
- })
- .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
- .then(text => {
- const
- xmlParser = new DOMParser(),
- responseList = getElementsByTagName(
- xmlParser.parseFromString(text, 'application/xml').documentElement,
- nsDAV,
- 'response');
- for (let i = 0; i < responseList.length; ++i) {
- const item = responseList.item(i);
- if (1 == getElementValue(item, nsNC, 'has-photo')) {
- [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)]
- .forEach(match => {
- ncAvatars.set(
- match[1].toLowerCase(),
- getElementValue(item, nsDAV, 'href') + '?photo'
- );
- });
- }
+ getElementsByTagName(parent, namespace, localName)?.item(0)?.textContent;
+ fetch(`${OC.webroot}/remote.php/dav/addressbooks/users/${OC.currentUser}/contacts/`, {
+ mode: 'same-origin',
+ cache: 'no-cache',
+ redirect: 'error',
+ credentials: 'same-origin',
+ method: 'REPORT',
+ headers: {
+ requesttoken: OC.requestToken,
+ 'Content-Type': 'application/xml; charset=utf-8',
+ Depth: 1
+ },
+ body: ''
+ })
+ .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
+ .then(text => {
+ const
+ xmlParser = new DOMParser(),
+ responseList = getElementsByTagName(
+ xmlParser.parseFromString(text, 'application/xml').documentElement,
+ nsDAV,
+ 'response');
+ for (let i = 0; i < responseList.length; ++i) {
+ const item = responseList.item(i);
+ if (1 == getElementValue(item, nsNC, 'has-photo')) {
+ [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)]
+ .forEach(match => {
+ ncAvatars.set(
+ match[1].toLowerCase(),
+ getElementValue(item, nsDAV, 'href') + '?photo'
+ );
+ });
}
- });
- }
+ }
+ });
}
- });
+// });
+ /**
+ * Used by MailMessageList
+ */
ko.bindingHandlers.fromPic = {
init: (element, self, dummy, msg) => {
try {
if (msg?.from?.[0]) {
let url = getAvatar(msg),
- from = msg.from[0],
fn = url=>{element.src = url};
+ element.onerror = ()=>{
+ element.onerror = null;
+ setIdenticon(msg, fn, uri=>avatars.set(getAvatarUid(msg), uri));
+ };
if (url) {
fn(url);
- } else if (msg.avatar) {
- if (msg.avatar?.startsWith('data:')) {
- fn(msg.avatar);
- } else {
- element.onerror = () => setIdenticon(from, fn);
- fn(getAvatarUrl(msg));
- }
+ } else if (msg.avatar?.startsWith('data:')) {
+ fn(msg.avatar);
} else {
+ element.onload = ()=>{
+ if (!element.src.startsWith('data:')) {
+ element.onload = null;
+ avatars.set(getAvatarUid(msg), element.src);
+ }
+ };
addQueue(msg, fn);
}
}
@@ -193,17 +225,6 @@
addEventListener('rl-view-model.create', e => {
if (templateId === e.detail.viewModelTemplateID) {
-
- const
- template = getEl(templateId),
- messageItemHeader = template.content.querySelector('.messageItemHeader');
-
- if (messageItemHeader) {
- messageItemHeader.prepend(Element.fromHTML(
- `
`
- ));
- }
-
let view = e.detail;
view.viewUserPic = ko.observable('');
view.viewUserPicVisible = ko.observable(false);
@@ -219,21 +240,13 @@
if (url) {
fn(url);
} else if (msg.avatar) {
- fn(msg.avatar.startsWith('data:') ? msg.avatar : getAvatarUrl(msg));
+ fn(getAvatarUri(msg));
} else {
-// let from = msg.from[0];
-// view.viewUserPic(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${encodeURIComponent(from.email)}`);
-// view.viewUserPicVisible(true);
addQueue(msg, fn);
}
}
});
}
-
- if ('MailMessageList' === e.detail.viewModelTemplateID) {
- getEl('MailMessageList').content.querySelectorAll('.messageCheckbox')
- .forEach(el => el.append(Element.fromHTML(`
`)));
- }
});
})(window.rl);
diff --git a/plugins/avatars/images/services/amazon.com.png b/plugins/avatars/images/services/amazon.com.png
index 4717c09c7..ec102e472 100644
Binary files a/plugins/avatars/images/services/amazon.com.png and b/plugins/avatars/images/services/amazon.com.png differ
diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php
index a43400e54..459172ebe 100644
--- a/plugins/avatars/index.php
+++ b/plugins/avatars/index.php
@@ -10,12 +10,12 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Avatars',
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
- VERSION = '1.16',
- RELEASE = '2024-03-12',
- REQUIRED = '2.25.0',
+ VERSION = '1.22',
+ RELEASE = '2025-03-10',
+ REQUIRED = '2.33.0',
CATEGORY = 'Contacts',
LICENSE = 'MIT',
- DESCRIPTION = 'Show graphic of sender in message and messages list (supports BIMI, Gravatar and identicon, Contacts is still TODO)';
+ DESCRIPTION = 'Show graphic of sender in message and messages list (supports Contacts, BIMI, Gravatar, favicon and identicon)';
public function Init() : void
{
@@ -28,7 +28,12 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addJs("{$identicon}.js");
}
// https://github.com/the-djmaze/snappymail/issues/714
- if ($this->Config()->Get('plugin', 'service', true) || !$this->Config()->Get('plugin', 'delay', true)) {
+ if ($this->Config()->Get('plugin', 'service', true)
+// || !$this->Config()->Get('plugin', 'delay', true)
+ || $this->Config()->Get('plugin', 'gravatar', false)
+ || $this->Config()->Get('plugin', 'bimi', false)
+ || $this->Config()->Get('plugin', 'favicon', false)
+ ) {
$this->addHook('json.after-message', 'JsonMessage');
$this->addHook('json.after-messagelist', 'JsonMessageList');
}
@@ -69,15 +74,13 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
$mFrom = $mFrom->jsonSerialize();
}
if (\is_array($mFrom)) {
- if (!$this->Config()->Get('plugin', 'delay', true)
- && ($this->Config()->Get('plugin', 'gravatar', false)
+ if (/*!$this->Config()->Get('plugin', 'delay', true)
+ && */($this->Config()->Get('plugin', 'gravatar', false)
|| ($this->Config()->Get('plugin', 'bimi', false) && 'pass' == $mFrom['dkimStatus'])
+ || ($this->Config()->Get('plugin', 'favicon', false) && 'pass' == $mFrom['dkimStatus'])
)
- ) try {
- // Base64Url
- return \SnappyMail\Crypt::EncryptUrlSafe($mFrom['email']);
- } catch (\Throwable $e) {
- \SnappyMail\Log::error('Crypt', $e->getMessage());
+ ) {
+ return 'remote';
}
if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) {
// 'data:image/png;base64,[a-zA-Z0-9+/=]'
@@ -106,16 +109,20 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
}
/**
- * GET /?Avatar/${bimi}/Encrypted(${from.email})
- * Nextcloud Mail uses insecure unencrypted 'index.php/apps/mail/api/avatars/url/local%40example.com'
+ * GET /?Avatar/${bimi}/Encoded(${from.email})
+ * Nextcloud Mail uses insecure unencoded 'index.php/apps/mail/api/avatars/url/local%40example.com'
*/
// public function ServiceAvatar(...$aParts)
- public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail)
+ public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEncodedEmail)
{
- $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEmail);
+ $maxAge = 86400;
+ $sEmail = \MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedEmail);
$aBimi = \explode('-', $sBimi, 2);
$sBimiSelector = isset($aBimi[1]) ? $aBimi[1] : 'default';
+// $sEmail && \MailSo\Base\Http::setETag("{$sBimiSelector}-{$sEncodedEmail}");
if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($aBimi[0]), $sBimiSelector))) {
+ \header("Cache-Control: max-age={$maxAge}, private");
+ \header('Expires: '.\gmdate('D, j M Y H:i:s', $maxAge + \time()).' UTC');
\header('Content-Type: '.$aResult[0]);
echo $aResult[1];
} else {
@@ -132,14 +139,20 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetAllowedInJs(true)
->SetDefaultValue(true),
+ \RainLoop\Plugins\Property::NewInstance('contacts')->SetLabel('Contacts')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDescription('Use the PHOTO stored on a matching address book contact')
+ ->SetDefaultValue(true),
\RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('BIMI')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
-// ->SetAllowedInJs(true)
->SetDefaultValue(false)
->SetDescription('https://bimigroup.org/ (DKIM header must be valid)'),
+ \RainLoop\Plugins\Property::NewInstance('favicon')->SetLabel('Favicon')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(false)
+ ->SetDescription('Fetch favicon from domain (DKIM header must be valid)'),
\RainLoop\Plugins\Property::NewInstance('gravatar')->SetLabel('Gravatar')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
-// ->SetAllowedInJs(true)
->SetDefaultValue(false)
->SetDescription('https://wikipedia.org/wiki/Gravatar'),
]);
@@ -147,7 +160,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
defined('RainLoop\\Enumerations\\PluginPropertyType::SELECT')
? \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Identicon')
->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECT)
-// ->SetAllowedInJs(true)
->SetDefaultValue([
['id' => '', 'name' => 'Name characters else silhouette'],
['id' => 'identicon', 'name' => 'Name characters else squares'],
@@ -156,7 +168,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetDescription('https://wikipedia.org/wiki/Identicon')
: \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Identicon')
->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION)
-// ->SetAllowedInJs(true)
->SetDefaultValue(['','identicon','jdenticon'])
->SetDescription('empty = default, identicon = squares, jdenticon = Triangles shape')
,
@@ -178,11 +189,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
return $aResult;
}
- // Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid.
- private static function getServiceIcon(string $sEmail) : ?string
+ private static function getServicePng(string $sDomain) : ?string
{
- $sDomain = \explode('@', $sEmail);
- $sDomain = \array_pop($sDomain);
$aServices = [
"services/{$sDomain}",
'services/' . static::serviceDomain($sDomain)
@@ -190,9 +198,20 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
foreach ($aServices as $service) {
$file = __DIR__ . "/images/{$service}.png";
if (\file_exists($file)) {
- return 'data:image/png;base64,' . \base64_encode(\file_get_contents($file));
+ return $file;
}
}
+ return null;
+ }
+
+ // Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid.
+ private static function getServiceIcon(string $sEmail) : ?string
+ {
+ $aParts = \explode('@', $sEmail);
+ $file = static::getServicePng(\array_pop($aParts));
+ if ($file) {
+ return 'data:image/png;base64,' . \base64_encode(\file_get_contents($file));
+ }
$aResult = static::getCachedImage($sEmail);
if ($aResult) {
@@ -215,30 +234,23 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
\header('Cache-Control: private');
// \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + 86400).' UTC');
+ // A photo stored on the contact is authoritative, so it is consulted
+ // before the cache and before any network lookup: it beats a guessed
+ // avatar and replaces up to five remote round-trips. Deliberately not
+ // written to the avatars cache, which has no expiry - a cached copy
+ // would shadow later edits to the contact.
+ if ($this->Config()->Get('plugin', 'contacts', true)) {
+ $aResult = static::getContactPhoto($sAsciiEmail);
+ if ($aResult) {
+ return $aResult;
+ }
+ }
+
$aResult = static::getCachedImage($sEmail);
if ($aResult) {
return $aResult;
}
- // TODO: lookup contacts vCard and return PHOTO value
- /*
- if (!$aResult) {
- $oActions = \RainLoop\Api::Actions();
- $oAccount = $oActions->getAccountFromToken();
- if ($oAccount) {
- $oAddressBookProvider = $oActions->AddressBookProvider($oAccount);
- if ($oAddressBookProvider) {
- $oContact = $oAddressBookProvider->GetContactByEmail($sEmail);
- if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) {
- $aResult = [
- 'text/vcard',
- $oContact->vCard
- ];
- }
- }
- }
- }
- */
if (!$aResult) {
$sDomain = \explode('@', $sEmail);
@@ -258,7 +270,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
}
if ($this->Config()->Get('plugin', 'gravatar', false)) {
- $aUrls[] = 'https://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404';
+ $aUrls[] = 'https://gravatar.com/avatar/'.\hash('sha256', \strtolower($sAsciiEmail)).'?s=80&d=404';
}
foreach ($aUrls as $sUrl) {
@@ -274,26 +286,18 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
// Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid.
if ($bBimi && !$aResult) {
- $aServices = [
- "services/{$sDomain}",
- 'services/' . static::serviceDomain($sDomain)
- ];
- foreach ($aServices as $service) {
- $file = __DIR__ . "/images/{$service}.png";
- if (\file_exists($file)) {
- \MailSo\Base\Http::setLastModified(\filemtime($file));
- $aResult = [
- 'image/png',
- \file_get_contents($file)
- ];
- break;
- }
+ $file = static::getServicePng($sDomain);
+ if ($file) {
+ \MailSo\Base\Http::setLastModified(\filemtime($file));
+ $aResult = [
+ 'image/png',
+ \file_get_contents($file)
+ ];
}
-/*
- if (!$aResult) {
- $aResult = static::getFavicon($sEmail, $sDomain);
+
+ if (!$aResult && $this->Config()->Get('plugin', 'favicon', false)) {
+ $aResult = static::getFavicon($sDomain);
}
-*/
}
return $aResult;
@@ -304,16 +308,91 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
$sDomain = \preg_replace('/^(.+\\.)?(paypal\\.[a-z][a-z])$/D', 'paypal.com', $sDomain);
$sDomain = \preg_replace('/^facebookmail.com$/D', 'facebook.com', $sDomain);
$sDomain = \preg_replace('/^dhlparcel.nl$/D', 'dhl.com', $sDomain);
+ $sDomain = \preg_replace('/^amazon.nl$/D', 'amazon.com', $sDomain);
$sDomain = \preg_replace('/^.+\\.([^.]+\\.[^.]+)$/D', '$1', $sDomain);
return $sDomain;
}
+ /**
+ * Return the PHOTO of the address book contact owning this address.
+ *
+ * vCard 4.0 stores it as a data: URI, vCard 3.0 as ENCODING=b with a TYPE
+ * parameter, and either version may point at an external URL instead.
+ */
+ private static function getContactPhoto(string $sEmail) : ?array
+ {
+ try {
+ $oActions = \RainLoop\Api::Actions();
+ $oAccount = $oActions->getAccountFromToken(false);
+ if (!$oAccount) {
+ return null;
+ }
+ $oProvider = $oActions->AddressBookProvider($oAccount);
+ if (!$oProvider || !$oProvider->IsActive()) {
+ return null;
+ }
+ // Several contacts can share an address - a synced card, a manual
+ // entry, an auto-collected one - and only one of them may carry the
+ // photo. GetContactByEmail() returns whichever matches first, so
+ // fall back to scanning the matches for one that actually has it.
+ $oPhoto = null;
+ $oContact = $oProvider->GetContactByEmail($sEmail);
+ if ($oContact && $oContact->vCard && isset($oContact->vCard->PHOTO)) {
+ $oPhoto = $oContact->vCard->PHOTO;
+ } else {
+ $iCount = 0;
+ foreach ($oProvider->GetContacts(0, 20, $sEmail, $iCount) as $oCandidate) {
+ if ($oCandidate->vCard && isset($oCandidate->vCard->PHOTO)) {
+ $oPhoto = $oCandidate->vCard->PHOTO;
+ break;
+ }
+ }
+ }
+ if (!$oPhoto) {
+ return null;
+ }
+
+ $sValue = \trim((string) $oPhoto);
+ if (!\strlen($sValue)) {
+ return null;
+ }
+
+ // vCard 4.0: data:image/jpeg;base64,...
+ if (\preg_match('#^data:(image/[a-z0-9.+-]+);base64,(.+)$#si', $sValue, $aMatch)) {
+ $sBinary = \base64_decode(\preg_replace('#\s+#', '', $aMatch[2]), true);
+ return $sBinary ? [\strtolower($aMatch[1]), $sBinary] : null;
+ }
+
+ // vCard 3.0: PHOTO;ENCODING=b;TYPE=JPEG:
+ $sEncoding = \strtolower((string) ($oPhoto['ENCODING'] ?? ''));
+ if ('b' === $sEncoding || 'base64' === $sEncoding) {
+ $sBinary = \base64_decode(\preg_replace('#\s+#', '', $sValue), true);
+ if ($sBinary) {
+ $sType = \strtolower((string) ($oPhoto['TYPE'] ?? '')) ?: 'jpeg';
+ return [\str_starts_with($sType, 'image/') ? $sType : "image/{$sType}", $sBinary];
+ }
+ return null;
+ }
+
+ // Either version may reference the image instead of embedding it.
+ if (\preg_match('#^https?://#i', $sValue)) {
+ return static::getUrl($sValue);
+ }
+ } catch (\Throwable $oException) {
+ \SnappyMail\Log::notice('Avatar', 'contact photo lookup failed: ' . $oException->getMessage());
+ }
+ return null;
+ }
+
private static function cacheImage(string $sEmail, array $aResult) : void
{
- $sEmailId = \sha1(\mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)));
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {
\mkdir(\APP_PRIVATE_DATA . 'avatars', 0700);
}
+ $sEmailId = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
+ if (\str_contains($sEmail, '@')) {
+ $sEmailId = \sha1($sEmailId);
+ }
\file_put_contents(
\APP_PRIVATE_DATA . 'avatars/' . $sEmailId . \SnappyMail\File\MimeType::toExtension($aResult[0]),
$aResult[1]
@@ -325,33 +404,61 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
$sEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
$aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*");
+ if (!$aFiles && \str_contains($sEmail, '@')) {
+ $sEmailId = \sha1($sEmail);
+ $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmailId}.*");
+ if (!$aFiles) {
+ $sDomain = \explode('@', $sEmail);
+ $sDomain = \array_pop($sDomain);
+ $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sDomain}.*");
+ }
+ }
if ($aFiles) {
- \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0]));
return [
\SnappyMail\File\MimeType::fromFile($aFiles[0]),
\file_get_contents($aFiles[0])
];
}
- $sEmailId = \sha1($sEmail);
- $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmailId}.*");
- if ($aFiles) {
- \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0]));
- return [
- \mime_content_type($aFiles[0]),
- \file_get_contents($aFiles[0])
- ];
- }
return null;
}
- private static function getFavicon(string $sEmail, string $sDomain) : ?array
+ private static function getFavicon(string $sDomain) : ?array
{
$aResult = static::getUrl('https://' . $sDomain . '/favicon.ico')
?: static::getUrl('https://' . static::serviceDomain($sDomain) . '/favicon.ico')
- ?: static::getUrl('https://www.' . static::serviceDomain($sDomain) . '/favicon.ico');
- // Also detect
+ ?: static::getUrl('https://www.' . static::serviceDomain($sDomain) . '/favicon.ico')
+ ?: static::getUrl("https://www.google.com/s2/favicons?sz=48&domain_url={$sDomain}")
+ ?: static::getUrl("https://api.faviconkit.com/{$sDomain}/48")
+// ?: static::getUrl("https://api.statvoo.com/favicon/{$sDomain}")
+ ;
+/*
+ Also detect the following?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+*/
if ($aResult) {
- static::cacheImage($sEmail, $aResult);
+ static::cacheImage($sDomain, $aResult);
}
return $aResult;
}
diff --git a/plugins/backup/index.php b/plugins/backup/index.php
index c59b23d2f..1dc65f475 100644
--- a/plugins/backup/index.php
+++ b/plugins/backup/index.php
@@ -6,8 +6,8 @@ class BackupPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Backup',
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
- VERSION = '1.1',
- RELEASE = '2023-12-12',
+ VERSION = '1.2',
+ RELEASE = '2024-03-18',
REQUIRED = '2.30.0',
CATEGORY = 'General',
LICENSE = 'MIT',
@@ -17,12 +17,12 @@ class BackupPlugin extends \RainLoop\Plugins\AbstractPlugin
{
// Admin Settings tab
$this->addJs('js/BackupAdminSettings.js', true); // add js file
- $this->addJsonHook('JsonAdminGetData');
+ $this->addJsonHook('JsonAdminBackupData');
$this->addJsonHook('JsonAdminRestoreData');
$this->addTemplate('templates/BackupAdminSettingsTab.html', true);
}
- public function JsonAdminGetData()
+ public function JsonAdminBackupData()
{
if (!($this->Manager()->Actions() instanceof \RainLoop\ActionsAdmin)
|| !$this->Manager()->Actions()->IsAdminLoggined()
@@ -79,18 +79,17 @@ class BackupPlugin extends \RainLoop\Plugins\AbstractPlugin
return $this->jsonResponse(__FUNCTION__, false);
}
+ $result = false;
if (\class_exists('ZipArchive')) {
$oArchive = new \ZipArchive();
$oArchive->open($_FILES['backup']['tmp_name'], \ZIPARCHIVE::CREATE);
- $oArchive->extractTo(APP_PRIVATE_DATA);
+ $result = $oArchive->extractTo(APP_PRIVATE_DATA);
} else if (\class_exists('PharData')) {
$oArchive = new \PharData($sTmp, 0, null, \Phar::GZ);
- $oArchive->extractTo(APP_PRIVATE_DATA);
+ $result = $oArchive->extractTo(APP_PRIVATE_DATA);
}
- return $this->jsonResponse(__FUNCTION__, array(
- '$_FILES' => $_FILES
- ));
+ return $this->jsonResponse(__FUNCTION__, $result);
}
}
diff --git a/plugins/backup/js/BackupAdminSettings.js b/plugins/backup/js/BackupAdminSettings.js
index ae8f2d489..221b5d1f0 100644
--- a/plugins/backup/js/BackupAdminSettings.js
+++ b/plugins/backup/js/BackupAdminSettings.js
@@ -30,7 +30,7 @@
link.remove();
}
- }, 'JsonAdminGetData');
+ }, 'JsonAdminBackupData');
}
submitForm(form) {
diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php b/plugins/cache-apcu/APCU.php
similarity index 87%
rename from snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php
rename to plugins/cache-apcu/APCU.php
index 09162268c..8ca581ca0 100644
--- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php
+++ b/plugins/cache-apcu/APCU.php
@@ -20,11 +20,12 @@ class APCU implements \MailSo\Cache\DriverInterface
{
private string $sKeyPrefix;
- function __construct(string $sKeyPrefix = '')
+ public function setPrefix(string $sKeyPrefix) : void
{
+ $sKeyPrefix = \rtrim(\trim($sKeyPrefix), '\\/');
$this->sKeyPrefix = empty($sKeyPrefix)
? $sKeyPrefix
- : \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($sKeyPrefix), '\\/')).'/';
+ : \preg_replace('/[^a-zA-Z0-9_]/', '_', $sKeyPrefix).'/';
}
public function Set(string $sKey, string $sValue) : bool
diff --git a/plugins/cache-apcu/index.php b/plugins/cache-apcu/index.php
new file mode 100644
index 000000000..2e17413a4
--- /dev/null
+++ b/plugins/cache-apcu/index.php
@@ -0,0 +1,34 @@
+addHook('main.fabrica', 'MainFabrica');
+ }
+ }
+
+ public function Supported() : string
+ {
+ return \MailSo\Base\Utils::FunctionsCallable(array('apcu_store', 'apcu_fetch', 'apcu_delete', 'apcu_clear_cache'))
+ ? ''
+ : 'PHP APCu not installed';
+ }
+
+ public function MainFabrica($sName, &$mResult)
+ {
+ if ('cache' == $sName) {
+ require_once __DIR__ . '/APCU.php';
+ $mResult = new \MailSo\Cache\Drivers\APCU;
+ }
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php b/plugins/cache-memcache/Memcache.php
similarity index 81%
rename from snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php
rename to plugins/cache-memcache/Memcache.php
index 60ab3a667..30a2812c2 100644
--- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php
+++ b/plugins/cache-memcache/Memcache.php
@@ -27,7 +27,7 @@ class Memcache implements \MailSo\Cache\DriverInterface
private string $sKeyPrefix;
- function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200, string $sKeyPrefix = '')
+ function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200)
{
$this->iExpire = 0 < $iExpire ? $iExpire : 43200;
@@ -35,15 +35,22 @@ class Memcache implements \MailSo\Cache\DriverInterface
if (!$this->oMem->addServer($sHost, \strpos($sHost, ':/') ? 0 : $iPort)) {
$this->oMem = null;
}
+ }
+ public function setPrefix(string $sKeyPrefix) : void
+ {
+ $sKeyPrefix = \rtrim(\trim($sKeyPrefix), '\\/');
$this->sKeyPrefix = empty($sKeyPrefix)
? $sKeyPrefix
- : \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($this->sKeyPrefix), '\\/')) . '/';
+ : \preg_replace('/[^a-zA-Z0-9_]/', '_', $sKeyPrefix).'/';
}
public function Set(string $sKey, string $sValue) : bool
{
- return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire) : false;
+ if ($this->oMem instanceof \Memcache) {
+ return $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire);
+ }
+ return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, $this->iExpire) : false;
}
public function Exists(string $sKey) : bool
diff --git a/plugins/cache-memcache/index.php b/plugins/cache-memcache/index.php
new file mode 100644
index 000000000..7760ab398
--- /dev/null
+++ b/plugins/cache-memcache/index.php
@@ -0,0 +1,54 @@
+addHook('main.fabrica', 'MainFabrica');
+ }
+ }
+
+ public function Supported() : string
+ {
+ return (\class_exists('Memcache',false) || \class_exists('Memcached',false))
+ ? ''
+ : 'PHP Memcache/Memcached not installed';
+ }
+
+ public function MainFabrica($sName, &$mResult)
+ {
+ if ('cache' == $sName) {
+ require_once __DIR__ . '/Memcache.php';
+ $mResult = new \MailSo\Cache\Drivers\Memcache(
+ $this->Config()->Get('plugin', 'host', '127.0.0.1'),
+ (int) $this->Config()->Get('plugin', 'port', 11211)
+ );
+ }
+ }
+
+ protected function configMapping() : array
+ {
+ return array(
+ \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('Host')
+ ->SetDescription('Hostname of the memcache server')
+ ->SetDefaultValue('127.0.0.1'),
+ \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('Port')
+ ->SetDescription('Port of the memcache server')
+ ->SetDefaultValue(11211)
+/*
+ ,\RainLoop\Plugins\Property::NewInstance('password')->SetLabel('Password')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD)
+ ->SetDefaultValue('')
+*/
+ );
+ }
+}
diff --git a/plugins/cache-redis/LICENSE b/plugins/cache-redis/LICENSE
new file mode 100644
index 000000000..9a8cd865b
--- /dev/null
+++ b/plugins/cache-redis/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2009-2020 Daniele Alessandri (original work)
+Copyright (c) 2021-2023 Till Krüss (modified work)
+
+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/cache-redis/Predis/Autoloader.php b/plugins/cache-redis/Predis/Autoloader.php
new file mode 100644
index 000000000..054f7bbb2
--- /dev/null
+++ b/plugins/cache-redis/Predis/Autoloader.php
@@ -0,0 +1,64 @@
+
+ * @author Daniele Alessandri
+ * @codeCoverageIgnore
+ */
+class Autoloader
+{
+ private $directory;
+ private $prefix;
+ private $prefixLength;
+
+ /**
+ * @param string $baseDirectory Base directory where the source files are located.
+ */
+ public function __construct($baseDirectory = __DIR__)
+ {
+ $this->directory = $baseDirectory;
+ $this->prefix = __NAMESPACE__ . '\\';
+ $this->prefixLength = strlen($this->prefix);
+ }
+
+ /**
+ * Registers the autoloader class with the PHP SPL autoloader.
+ *
+ * @param bool $prepend Prepend the autoloader on the stack instead of appending it.
+ */
+ public static function register($prepend = false)
+ {
+ spl_autoload_register([new self(), 'autoload'], true, $prepend);
+ }
+
+ /**
+ * Loads a class from a file using its fully qualified name.
+ *
+ * @param string $className Fully qualified name of a class.
+ */
+ public function autoload($className)
+ {
+ if (0 === strpos($className, $this->prefix)) {
+ $parts = explode('\\', substr($className, $this->prefixLength));
+ $filepath = $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts) . '.php';
+
+ if (is_file($filepath)) {
+ require $filepath;
+ }
+ }
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Client.php b/plugins/cache-redis/Predis/Client.php
similarity index 50%
rename from snappymail/v/0.0.0/app/libraries/Predis/Client.php
rename to plugins/cache-redis/Predis/Client.php
index 87596ec92..4257667bd 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Client.php
+++ b/plugins/cache-redis/Predis/Client.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,21 +12,35 @@
namespace Predis;
+use ArrayIterator;
+use InvalidArgumentException;
+use IteratorAggregate;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
+use Predis\Command\Redis\Container\ContainerFactory;
+use Predis\Command\Redis\Container\ContainerInterface;
use Predis\Command\ScriptCommand;
use Predis\Configuration\Options;
use Predis\Configuration\OptionsInterface;
-use Predis\Connection\AggregateConnectionInterface;
use Predis\Connection\ConnectionInterface;
+use Predis\Connection\Parameters;
use Predis\Connection\ParametersInterface;
+use Predis\Connection\RelayConnection;
use Predis\Monitor\Consumer as MonitorConsumer;
+use Predis\Pipeline\Atomic;
+use Predis\Pipeline\FireAndForget;
use Predis\Pipeline\Pipeline;
+use Predis\Pipeline\RelayAtomic;
+use Predis\Pipeline\RelayPipeline;
use Predis\PubSub\Consumer as PubSubConsumer;
+use Predis\PubSub\RelayConsumer as RelayPubSubConsumer;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ResponseInterface;
use Predis\Response\ServerException;
use Predis\Transaction\MultiExec as MultiExecTransaction;
+use ReturnTypeWillChange;
+use RuntimeException;
+use Traversable;
/**
* Client class used for connecting and executing commands on Redis.
@@ -34,17 +49,20 @@ use Predis\Transaction\MultiExec as MultiExecTransaction;
* abstractions are built. Internally it aggregates various other classes each
* one with its own responsibility and scope.
*
- * {@inheritdoc}
- *
- * @author Daniele Alessandri
+ * @template-implements \IteratorAggregate
*/
-class Client implements ClientInterface
+class Client implements ClientInterface, IteratorAggregate
{
- const VERSION = '1.0.3';
+ public const VERSION = '2.2.2';
- protected $connection;
- protected $options;
- private $profile;
+ /** @var OptionsInterface */
+ private $options;
+
+ /** @var ConnectionInterface */
+ private $connection;
+
+ /** @var Command\FactoryInterface */
+ private $commands;
/**
* @param mixed $parameters Connection parameters for one or more servers.
@@ -52,126 +70,99 @@ class Client implements ClientInterface
*/
public function __construct($parameters = null, $options = null)
{
- $this->options = $this->createOptions($options ?: array());
- $this->connection = $this->createConnection($parameters ?: array());
- $this->profile = $this->options->profile;
+ $this->options = static::createOptions($options ?? new Options());
+ $this->connection = static::createConnection($this->options, $parameters ?? new Parameters());
+ $this->commands = $this->options->commands;
}
/**
- * Creates a new instance of Predis\Configuration\Options from different
- * types of arguments or simply returns the passed argument if it is an
- * instance of Predis\Configuration\OptionsInterface.
+ * Creates a new set of client options for the client.
*
- * @param mixed $options Client options.
- *
- * @throws \InvalidArgumentException
+ * @param array|OptionsInterface $options Set of client options
*
* @return OptionsInterface
+ * @throws InvalidArgumentException
*/
- protected function createOptions($options)
+ protected static function createOptions($options)
{
if (is_array($options)) {
return new Options($options);
- }
-
- if ($options instanceof OptionsInterface) {
+ } elseif ($options instanceof OptionsInterface) {
return $options;
+ } else {
+ throw new InvalidArgumentException('Invalid type for client options');
}
-
- throw new \InvalidArgumentException('Invalid type for client options.');
}
/**
- * Creates single or aggregate connections from different types of arguments
- * (string, array) or returns the passed argument if it is an instance of a
- * class implementing Predis\Connection\ConnectionInterface.
+ * Creates single or aggregate connections from supplied arguments.
*
- * Accepted types for connection parameters are:
+ * This method accepts the following types to create a connection instance:
*
- * - Instance of Predis\Connection\ConnectionInterface.
- * - Instance of Predis\Connection\ParametersInterface.
- * - Array
- * - String
- * - Callable
+ * - Array (dictionary: single connection, indexed: aggregate connections)
+ * - String (URI for a single connection)
+ * - Callable (connection initializer callback)
+ * - Instance of Predis\Connection\ParametersInterface (used as-is)
+ * - Instance of Predis\Connection\ConnectionInterface (returned as-is)
*
- * @param mixed $parameters Connection parameters or connection instance.
+ * When a callable is passed, it receives the original set of client options
+ * and must return an instance of Predis\Connection\ConnectionInterface.
*
- * @throws \InvalidArgumentException
+ * Connections are created using the connection factory (in case of single
+ * connections) or a specialized aggregate connection initializer (in case
+ * of cluster and replication) retrieved from the supplied client options.
+ *
+ * @param OptionsInterface $options Client options container
+ * @param mixed $parameters Connection parameters
*
* @return ConnectionInterface
+ * @throws InvalidArgumentException
*/
- protected function createConnection($parameters)
+ protected static function createConnection(OptionsInterface $options, $parameters)
{
if ($parameters instanceof ConnectionInterface) {
return $parameters;
}
if ($parameters instanceof ParametersInterface || is_string($parameters)) {
- return $this->options->connections->create($parameters);
+ return $options->connections->create($parameters);
}
if (is_array($parameters)) {
if (!isset($parameters[0])) {
- return $this->options->connections->create($parameters);
- }
-
- $options = $this->options;
-
- if ($options->defined('aggregate')) {
- $initializer = $this->getConnectionInitializerWrapper($options->aggregate);
- $connection = $initializer($parameters, $options);
+ return $options->connections->create($parameters);
+ } elseif ($options->defined('cluster') && $initializer = $options->cluster) {
+ return $initializer($parameters, true);
+ } elseif ($options->defined('replication') && $initializer = $options->replication) {
+ return $initializer($parameters, true);
+ } elseif ($options->defined('aggregate') && $initializer = $options->aggregate) {
+ return $initializer($parameters, false);
} else {
- if ($options->defined('replication') && $replication = $options->replication) {
- $connection = $replication;
- } else {
- $connection = $options->cluster;
- }
-
- $options->connections->aggregate($connection, $parameters);
+ throw new InvalidArgumentException(
+ 'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option'
+ );
}
-
- return $connection;
}
if (is_callable($parameters)) {
- $initializer = $this->getConnectionInitializerWrapper($parameters);
- $connection = $initializer($this->options);
+ $connection = call_user_func($parameters, $options);
+
+ if (!$connection instanceof ConnectionInterface) {
+ throw new InvalidArgumentException('Callable parameters must return a valid connection');
+ }
return $connection;
}
- throw new \InvalidArgumentException('Invalid type for connection parameters.');
- }
-
- /**
- * Wraps a callable to make sure that its returned value represents a valid
- * connection type.
- *
- * @param mixed $callable
- *
- * @return \Closure
- */
- protected function getConnectionInitializerWrapper($callable)
- {
- return function () use ($callable) {
- $connection = call_user_func_array($callable, func_get_args());
-
- if (!$connection instanceof ConnectionInterface) {
- throw new \UnexpectedValueException(
- 'The callable connection initializer returned an invalid type.'
- );
- }
-
- return $connection;
- };
+ throw new InvalidArgumentException('Invalid type for connection parameters');
}
/**
* {@inheritdoc}
*/
- public function getProfile()
+ public function getCommandFactory()
{
- return $this->profile;
+ return $this->commands;
}
/**
@@ -183,23 +174,53 @@ class Client implements ClientInterface
}
/**
- * Creates a new client instance for the specified connection ID or alias,
- * only when working with an aggregate connection (cluster, replication).
- * The new client instances uses the same options of the original one.
+ * Creates a new client using a specific underlying connection.
*
- * @param string $connectionID Identifier of a connection.
+ * This method allows to create a new client instance by picking a specific
+ * connection out of an aggregate one, with the same options of the original
+ * client instance.
*
- * @throws \InvalidArgumentException
+ * The specified selector defines which logic to use to look for a suitable
+ * connection by the specified value. Supported selectors are:
*
- * @return Client
+ * - `id`
+ * - `key`
+ * - `slot`
+ * - `command`
+ * - `alias`
+ * - `role`
+ *
+ * Internally the client relies on duck-typing and follows this convention:
+ *
+ * $selector string => getConnectionBy$selector($value) method
+ *
+ * This means that support for specific selectors may vary depending on the
+ * actual logic implemented by connection classes and there is no interface
+ * binding a connection class to implement any of these.
+ *
+ * @param string $selector Type of selector.
+ * @param mixed $value Value to be used by the selector.
+ *
+ * @return ClientInterface
*/
- public function getClientFor($connectionID)
+ public function getClientBy($selector, $value)
{
- if (!$connection = $this->getConnectionById($connectionID)) {
- throw new \InvalidArgumentException("Invalid connection ID: $connectionID.");
+ $selector = strtolower($selector);
+
+ if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) {
+ throw new InvalidArgumentException("Invalid selector type: `$selector`");
}
- return new static($connection, $this->options);
+ if (!method_exists($this->connection, $method = "getConnectionBy$selector")) {
+ $class = get_class($this->connection);
+ throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class");
+ }
+
+ if (!$connection = $this->connection->$method($value)) {
+ throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`");
+ }
+
+ return new static($connection, $this->getOptions());
}
/**
@@ -248,24 +269,29 @@ class Client implements ClientInterface
}
/**
- * Retrieves the specified connection from the aggregate connection when the
- * client is in cluster or replication mode.
+ * Applies the configured serializer and compression to given value.
*
- * @param string $connectionID Index or alias of the single connection.
- *
- * @throws NotSupportedException
- *
- * @return Connection\NodeConnectionInterface
+ * @param mixed $value
+ * @return string
*/
- public function getConnectionById($connectionID)
+ public function pack($value)
{
- if (!$this->connection instanceof AggregateConnectionInterface) {
- throw new NotSupportedException(
- 'Retrieving connections by ID is supported only by aggregate connections.'
- );
- }
+ return $this->connection instanceof RelayConnection
+ ? $this->connection->pack($value)
+ : $value;
+ }
- return $this->connection->getConnectionById($connectionID);
+ /**
+ * Deserializes and decompresses to given value.
+ *
+ * @param mixed $value
+ * @return string
+ */
+ public function unpack($value)
+ {
+ return $this->connection instanceof RelayConnection
+ ? $this->connection->unpack($value)
+ : $value;
}
/**
@@ -273,7 +299,7 @@ class Client implements ClientInterface
* applying any prefix to keys or throwing exceptions on Redis errors even
* regardless of client options.
*
- * It is possibile to indentify Redis error responses from normal responses
+ * It is possible to identify Redis error responses from normal responses
* using the second optional argument which is populated by reference.
*
* @param array $arguments Command arguments as defined by the command signature.
@@ -284,9 +310,10 @@ class Client implements ClientInterface
public function executeRaw(array $arguments, &$error = null)
{
$error = false;
+ $commandID = array_shift($arguments);
$response = $this->connection->executeCommand(
- new RawCommand($arguments)
+ new RawCommand($commandID, $arguments)
);
if ($response instanceof ResponseInterface) {
@@ -313,9 +340,37 @@ class Client implements ClientInterface
/**
* {@inheritdoc}
*/
- public function createCommand($commandID, $arguments = array())
+ public function createCommand($commandID, $arguments = [])
{
- return $this->profile->createCommand($commandID, $arguments);
+ return $this->commands->create($commandID, $arguments);
+ }
+
+ /**
+ * @param string $name
+ * @return ContainerInterface
+ */
+ public function __get(string $name)
+ {
+ return ContainerFactory::create($this, $name);
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ * @return mixed
+ */
+ public function __set(string $name, $value)
+ {
+ throw new RuntimeException('Not allowed');
+ }
+
+ /**
+ * @param string $name
+ * @return mixed
+ */
+ public function __isset(string $name)
+ {
+ throw new RuntimeException('Not allowed');
}
/**
@@ -342,17 +397,13 @@ class Client implements ClientInterface
* @param CommandInterface $command Redis command that generated the error.
* @param ErrorResponseInterface $response Instance of the error response.
*
- * @throws ServerException
- *
* @return mixed
+ * @throws ServerException
*/
protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response)
{
if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
- $eval = $this->createCommand('EVAL');
- $eval->setRawArguments($command->getEvalArguments());
-
- $response = $this->executeCommand($eval);
+ $response = $this->executeCommand($command->getEvalCommand());
if (!$response instanceof ResponseInterface) {
$response = $command->parseResponse($response);
@@ -370,7 +421,7 @@ class Client implements ClientInterface
/**
* Executes the specified initializer method on `$this` by adjusting the
- * actual invokation depending on the arity (0, 1 or 2 arguments). This is
+ * actual invocation depending on the arity (0, 1 or 2 arguments). This is
* simply an utility method to create Redis contexts instances since they
* follow a common initialization path.
*
@@ -391,7 +442,7 @@ class Client implements ClientInterface
: $this->$initializer(null, $argv[0]);
case 2:
- list($arg0, $arg1) = $argv;
+ [$arg0, $arg1] = $argv;
return $this->$initializer($arg0, $arg1);
@@ -404,11 +455,11 @@ class Client implements ClientInterface
* Creates a new pipeline context and returns it, or returns the results of
* a pipeline executed inside the optionally provided callable object.
*
- * @param mixed ... Array of options, a callable for execution, or both.
+ * @param mixed ...$arguments Array of options, a callable for execution, or both.
*
* @return Pipeline|array
*/
- public function pipeline(/* arguments */)
+ public function pipeline(...$arguments)
{
return $this->sharedContextFactory('createPipeline', func_get_args());
}
@@ -416,19 +467,29 @@ class Client implements ClientInterface
/**
* Actual pipeline context initializer method.
*
- * @param array $options Options for the context.
- * @param mixed $callable Optional callable used to execute the context.
+ * @param array|null $options Options for the context.
+ * @param mixed $callable Optional callable used to execute the context.
*
* @return Pipeline|array
*/
- protected function createPipeline(array $options = null, $callable = null)
+ protected function createPipeline(?array $options = null, $callable = null)
{
if (isset($options['atomic']) && $options['atomic']) {
- $class = 'Predis\Pipeline\Atomic';
+ $class = Atomic::class;
} elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
- $class = 'Predis\Pipeline\FireAndForget';
+ $class = FireAndForget::class;
} else {
- $class = 'Predis\Pipeline\Pipeline';
+ $class = Pipeline::class;
+ }
+
+ if ($this->connection instanceof RelayConnection) {
+ if (isset($options['atomic']) && $options['atomic']) {
+ $class = RelayAtomic::class;
+ } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
+ throw new NotSupportedException('The "relay" extension does not support fire-and-forget pipelines.');
+ } else {
+ $class = RelayPipeline::class;
+ }
}
/*
@@ -447,11 +508,11 @@ class Client implements ClientInterface
* Creates a new transaction context and returns it, or returns the results
* of a transaction executed inside the optionally provided callable object.
*
- * @param mixed ... Array of options, a callable for execution, or both.
+ * @param mixed ...$arguments Array of options, a callable for execution, or both.
*
* @return MultiExecTransaction|array
*/
- public function transaction(/* arguments */)
+ public function transaction(...$arguments)
{
return $this->sharedContextFactory('createTransaction', func_get_args());
}
@@ -464,7 +525,7 @@ class Client implements ClientInterface
*
* @return MultiExecTransaction|array
*/
- protected function createTransaction(array $options = null, $callable = null)
+ protected function createTransaction(?array $options = null, $callable = null)
{
$transaction = new MultiExecTransaction($this, $options);
@@ -476,14 +537,14 @@ class Client implements ClientInterface
}
/**
- * Creates a new publis/subscribe context and returns it, or starts its loop
+ * Creates a new publish/subscribe context and returns it, or starts its loop
* inside the optionally provided callable object.
*
- * @param mixed ... Array of options, a callable for execution, or both.
+ * @param mixed ...$arguments Array of options, a callable for execution, or both.
*
* @return PubSubConsumer|null
*/
- public function pubSubLoop(/* arguments */)
+ public function pubSubLoop(...$arguments)
{
return $this->sharedContextFactory('createPubSub', func_get_args());
}
@@ -496,9 +557,13 @@ class Client implements ClientInterface
*
* @return PubSubConsumer|null
*/
- protected function createPubSub(array $options = null, $callable = null)
+ protected function createPubSub(?array $options = null, $callable = null)
{
- $pubsub = new PubSubConsumer($this, $options);
+ if ($this->connection instanceof RelayConnection) {
+ $pubsub = new RelayPubSubConsumer($this, $options);
+ } else {
+ $pubsub = new PubSubConsumer($this, $options);
+ }
if (!isset($callable)) {
return $pubsub;
@@ -509,6 +574,8 @@ class Client implements ClientInterface
$pubsub->stop();
}
}
+
+ return null;
}
/**
@@ -520,4 +587,26 @@ class Client implements ClientInterface
{
return new MonitorConsumer($this);
}
+
+ /**
+ * @return Traversable
+ */
+ #[ReturnTypeWillChange]
+ public function getIterator()
+ {
+ $clients = [];
+ $connection = $this->getConnection();
+
+ if (!$connection instanceof Traversable) {
+ return new ArrayIterator([
+ (string) $connection => new static($connection, $this->getOptions()),
+ ]);
+ }
+
+ foreach ($connection as $node) {
+ $clients[(string) $node] = new static($node, $this->getOptions());
+ }
+
+ return new ArrayIterator($clients);
+ }
}
diff --git a/plugins/cache-redis/Predis/ClientConfiguration.php b/plugins/cache-redis/Predis/ClientConfiguration.php
new file mode 100644
index 000000000..c70cd61cf
--- /dev/null
+++ b/plugins/cache-redis/Predis/ClientConfiguration.php
@@ -0,0 +1,42 @@
+ [
+ ['name' => 'Json', 'commandPrefix' => 'JSON'],
+ ['name' => 'BloomFilter', 'commandPrefix' => 'BF'],
+ ['name' => 'CuckooFilter', 'commandPrefix' => 'CF'],
+ ['name' => 'CountMinSketch', 'commandPrefix' => 'CMS'],
+ ['name' => 'TDigest', 'commandPrefix' => 'TDIGEST'],
+ ['name' => 'TopK', 'commandPrefix' => 'TOPK'],
+ ['name' => 'Search', 'commandPrefix' => 'FT'],
+ ['name' => 'TimeSeries', 'commandPrefix' => 'TS'],
+ ],
+ ];
+
+ /**
+ * Returns available modules with configuration.
+ *
+ * @return array|string[][]
+ */
+ public static function getModules(): array
+ {
+ return self::$config['modules'];
+ }
+}
diff --git a/plugins/cache-redis/Predis/ClientContextInterface.php b/plugins/cache-redis/Predis/ClientContextInterface.php
new file mode 100644
index 000000000..e443ee5d1
--- /dev/null
+++ b/plugins/cache-redis/Predis/ClientContextInterface.php
@@ -0,0 +1,380 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,8 +14,6 @@ namespace Predis;
/**
* Exception class that identifies client-side errors.
- *
- * @author Daniele Alessandri
*/
class ClientException extends PredisException
{
diff --git a/plugins/cache-redis/Predis/ClientInterface.php b/plugins/cache-redis/Predis/ClientInterface.php
new file mode 100644
index 000000000..2937c5804
--- /dev/null
+++ b/plugins/cache-redis/Predis/ClientInterface.php
@@ -0,0 +1,431 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,21 +12,17 @@
namespace Predis\Cluster;
+use InvalidArgumentException;
use Predis\Command\CommandInterface;
use Predis\Command\ScriptCommand;
/**
* Common class implementing the logic needed to support clustering strategies.
- *
- * @author Daniele Alessandri
*/
abstract class ClusterStrategy implements StrategyInterface
{
protected $commands;
- /**
- *
- */
public function __construct()
{
$this->commands = $this->getDefaultCommands();
@@ -38,12 +35,12 @@ abstract class ClusterStrategy implements StrategyInterface
*/
protected function getDefaultCommands()
{
- $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
- $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');
+ $getKeyFromFirstArgument = [$this, 'getKeyFromFirstArgument'];
+ $getKeyFromAllArguments = [$this, 'getKeyFromAllArguments'];
- return array(
+ return [
/* commands operating on the key space */
- 'EXISTS' => $getKeyFromFirstArgument,
+ 'EXISTS' => $getKeyFromAllArguments,
'DEL' => $getKeyFromAllArguments,
'TYPE' => $getKeyFromFirstArgument,
'EXPIRE' => $getKeyFromFirstArgument,
@@ -53,9 +50,10 @@ abstract class ClusterStrategy implements StrategyInterface
'PEXPIREAT' => $getKeyFromFirstArgument,
'TTL' => $getKeyFromFirstArgument,
'PTTL' => $getKeyFromFirstArgument,
- 'SORT' => $getKeyFromFirstArgument, // TODO
+ 'SORT' => [$this, 'getKeyFromSortCommand'],
'DUMP' => $getKeyFromFirstArgument,
'RESTORE' => $getKeyFromFirstArgument,
+ 'FLUSHDB' => [$this, 'getFakeKey'],
/* commands operating on string values */
'APPEND' => $getKeyFromFirstArgument,
@@ -72,14 +70,15 @@ abstract class ClusterStrategy implements StrategyInterface
'INCRBYFLOAT' => $getKeyFromFirstArgument,
'SETBIT' => $getKeyFromFirstArgument,
'SETEX' => $getKeyFromFirstArgument,
- 'MSET' => array($this, 'getKeyFromInterleavedArguments'),
- 'MSETNX' => array($this, 'getKeyFromInterleavedArguments'),
+ 'MSET' => [$this, 'getKeyFromInterleavedArguments'],
+ 'MSETNX' => [$this, 'getKeyFromInterleavedArguments'],
'SETNX' => $getKeyFromFirstArgument,
'SETRANGE' => $getKeyFromFirstArgument,
'STRLEN' => $getKeyFromFirstArgument,
'SUBSTR' => $getKeyFromFirstArgument,
- 'BITOP' => array($this, 'getKeyFromBitOp'),
+ 'BITOP' => [$this, 'getKeyFromBitOp'],
'BITCOUNT' => $getKeyFromFirstArgument,
+ 'BITFIELD' => $getKeyFromFirstArgument,
/* commands operating on lists */
'LINSERT' => $getKeyFromFirstArgument,
@@ -88,9 +87,9 @@ abstract class ClusterStrategy implements StrategyInterface
'LPOP' => $getKeyFromFirstArgument,
'RPOP' => $getKeyFromFirstArgument,
'RPOPLPUSH' => $getKeyFromAllArguments,
- 'BLPOP' => array($this, 'getKeyFromBlockingListCommands'),
- 'BRPOP' => array($this, 'getKeyFromBlockingListCommands'),
- 'BRPOPLPUSH' => array($this, 'getKeyFromBlockingListCommands'),
+ 'BLPOP' => [$this, 'getKeyFromBlockingListCommands'],
+ 'BRPOP' => [$this, 'getKeyFromBlockingListCommands'],
+ 'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'],
'LPUSH' => $getKeyFromFirstArgument,
'LPUSHX' => $getKeyFromFirstArgument,
'RPUSH' => $getKeyFromFirstArgument,
@@ -121,7 +120,7 @@ abstract class ClusterStrategy implements StrategyInterface
'ZCARD' => $getKeyFromFirstArgument,
'ZCOUNT' => $getKeyFromFirstArgument,
'ZINCRBY' => $getKeyFromFirstArgument,
- 'ZINTERSTORE' => array($this, 'getKeyFromZsetAggregationCommands'),
+ 'ZINTERSTORE' => [$this, 'getKeyFromZsetAggregationCommands'],
'ZRANGE' => $getKeyFromFirstArgument,
'ZRANGEBYSCORE' => $getKeyFromFirstArgument,
'ZRANK' => $getKeyFromFirstArgument,
@@ -132,7 +131,7 @@ abstract class ClusterStrategy implements StrategyInterface
'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument,
'ZREVRANK' => $getKeyFromFirstArgument,
'ZSCORE' => $getKeyFromFirstArgument,
- 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'),
+ 'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'],
'ZSCAN' => $getKeyFromFirstArgument,
'ZLEXCOUNT' => $getKeyFromFirstArgument,
'ZRANGEBYLEX' => $getKeyFromFirstArgument,
@@ -162,9 +161,23 @@ abstract class ClusterStrategy implements StrategyInterface
'PFMERGE' => $getKeyFromAllArguments,
/* scripting */
- 'EVAL' => array($this, 'getKeyFromScriptingCommands'),
- 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'),
- );
+ 'EVAL' => [$this, 'getKeyFromScriptingCommands'],
+ 'EVALSHA' => [$this, 'getKeyFromScriptingCommands'],
+
+ /* server */
+ 'INFO' => [$this, 'getFakeKey'],
+
+ /* commands performing geospatial operations */
+ 'GEOADD' => $getKeyFromFirstArgument,
+ 'GEOHASH' => $getKeyFromFirstArgument,
+ 'GEOPOS' => $getKeyFromFirstArgument,
+ 'GEODIST' => $getKeyFromFirstArgument,
+ 'GEORADIUS' => [$this, 'getKeyFromGeoradiusCommands'],
+ 'GEORADIUSBYMEMBER' => [$this, 'getKeyFromGeoradiusCommands'],
+
+ /* cluster */
+ 'CLUSTER' => [$this, 'getFakeKey'],
+ ];
}
/**
@@ -189,7 +202,7 @@ abstract class ClusterStrategy implements StrategyInterface
* @param string $commandID Command ID.
* @param mixed $callback A valid callable object, or NULL to unset the handler.
*
- * @throws \InvalidArgumentException
+ * @throws InvalidArgumentException
*/
public function setCommandHandler($commandID, $callback = null)
{
@@ -202,7 +215,7 @@ abstract class ClusterStrategy implements StrategyInterface
}
if (!is_callable($callback)) {
- throw new \InvalidArgumentException(
+ throw new InvalidArgumentException(
'The argument must be a callable object or NULL.'
);
}
@@ -210,6 +223,16 @@ abstract class ClusterStrategy implements StrategyInterface
$this->commands[$commandID] = $callback;
}
+ /**
+ * Get fake key for commands with no key argument.
+ *
+ * @return string
+ */
+ protected function getFakeKey(): string
+ {
+ return 'key';
+ }
+
/**
* Extracts the key from the first argument of a command instance.
*
@@ -234,9 +257,11 @@ abstract class ClusterStrategy implements StrategyInterface
{
$arguments = $command->getArguments();
- if ($this->checkSameSlotForKeys($arguments)) {
- return $arguments[0];
+ if (!$this->checkSameSlotForKeys($arguments)) {
+ return null;
}
+
+ return $arguments[0];
}
/**
@@ -250,15 +275,48 @@ abstract class ClusterStrategy implements StrategyInterface
protected function getKeyFromInterleavedArguments(CommandInterface $command)
{
$arguments = $command->getArguments();
- $keys = array();
+ $keys = [];
for ($i = 0; $i < count($arguments); $i += 2) {
$keys[] = $arguments[$i];
}
- if ($this->checkSameSlotForKeys($keys)) {
- return $arguments[0];
+ if (!$this->checkSameSlotForKeys($keys)) {
+ return null;
}
+
+ return $arguments[0];
+ }
+
+ /**
+ * Extracts the key from SORT command.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromSortCommand(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+ $firstKey = $arguments[0];
+
+ if (1 === $argc = count($arguments)) {
+ return $firstKey;
+ }
+
+ $keys = [$firstKey];
+
+ for ($i = 1; $i < $argc; ++$i) {
+ if (strtoupper($arguments[$i]) === 'STORE') {
+ $keys[] = $arguments[++$i];
+ }
+ }
+
+ if (!$this->checkSameSlotForKeys($keys)) {
+ return null;
+ }
+
+ return $firstKey;
}
/**
@@ -272,9 +330,11 @@ abstract class ClusterStrategy implements StrategyInterface
{
$arguments = $command->getArguments();
- if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
- return $arguments[0];
+ if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
+ return null;
}
+
+ return $arguments[0];
}
/**
@@ -288,9 +348,42 @@ abstract class ClusterStrategy implements StrategyInterface
{
$arguments = $command->getArguments();
- if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
- return $arguments[1];
+ if (!$this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
+ return null;
}
+
+ return $arguments[1];
+ }
+
+ /**
+ * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands.
+ *
+ * @param CommandInterface $command Command instance.
+ *
+ * @return string|null
+ */
+ protected function getKeyFromGeoradiusCommands(CommandInterface $command)
+ {
+ $arguments = $command->getArguments();
+ $argc = count($arguments);
+ $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
+
+ if ($argc > $startIndex) {
+ $keys = [$arguments[0]];
+
+ for ($i = $startIndex; $i < $argc; ++$i) {
+ $argument = strtoupper($arguments[$i]);
+ if ($argument === 'STORE' || $argument === 'STOREDIST') {
+ $keys[] = $arguments[++$i];
+ }
+ }
+
+ if (!$this->checkSameSlotForKeys($keys)) {
+ return null;
+ }
+ }
+
+ return $arguments[0];
}
/**
@@ -303,11 +396,13 @@ abstract class ClusterStrategy implements StrategyInterface
protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
{
$arguments = $command->getArguments();
- $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1]));
+ $keys = array_merge([$arguments[0]], array_slice($arguments, 2, $arguments[1]));
- if ($this->checkSameSlotForKeys($keys)) {
- return $arguments[0];
+ if (!$this->checkSameSlotForKeys($keys)) {
+ return null;
}
+
+ return $arguments[0];
}
/**
@@ -319,15 +414,15 @@ abstract class ClusterStrategy implements StrategyInterface
*/
protected function getKeyFromScriptingCommands(CommandInterface $command)
{
- if ($command instanceof ScriptCommand) {
- $keys = $command->getKeys();
- } else {
- $keys = array_slice($args = $command->getArguments(), 2, $args[1]);
+ $keys = $command instanceof ScriptCommand
+ ? $command->getKeys()
+ : array_slice($args = $command->getArguments(), 2, $args[1]);
+
+ if (!$keys || !$this->checkSameSlotForKeys($keys)) {
+ return null;
}
- if ($keys && $this->checkSameSlotForKeys($keys)) {
- return $keys[0];
- }
+ return $keys[0];
}
/**
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php b/plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php
similarity index 94%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php
rename to plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php
index 831f52c52..593d9bb3c 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php
+++ b/plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -16,8 +17,6 @@ use Predis\Cluster\Hash\HashGeneratorInterface;
/**
* A distributor implements the logic to automatically distribute keys among
* several nodes for client-side sharding.
- *
- * @author Daniele Alessandri
*/
interface DistributorInterface
{
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/EmptyRingException.php b/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php
similarity index 66%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/EmptyRingException.php
rename to plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php
index 039f2f2e8..68172066e 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/EmptyRingException.php
+++ b/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,11 +12,11 @@
namespace Predis\Cluster\Distributor;
+use Exception;
+
/**
* Exception class that identifies empty rings.
- *
- * @author Daniele Alessandri
*/
-class EmptyRingException extends \Exception
+class EmptyRingException extends Exception
{
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php b/plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php
similarity index 94%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php
rename to plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php
index db864d912..03e75106f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php
+++ b/plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,21 +18,19 @@ use Predis\Cluster\Hash\HashGeneratorInterface;
* This class implements an hashring-based distributor that uses the same
* algorithm of memcache to distribute keys in a cluster using client-side
* sharding.
- *
- * @author Daniele Alessandri
* @author Lorenzo Castelli
*/
class HashRing implements DistributorInterface, HashGeneratorInterface
{
- const DEFAULT_REPLICAS = 128;
- const DEFAULT_WEIGHT = 100;
+ public const DEFAULT_REPLICAS = 128;
+ public const DEFAULT_WEIGHT = 100;
private $ring;
private $ringKeys;
private $ringKeysCount;
private $replicas;
private $nodeHashCallback;
- private $nodes = array();
+ private $nodes = [];
/**
* @param int $replicas Number of replicas in the ring.
@@ -53,10 +52,10 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
{
// In case of collisions in the hashes of the nodes, the node added
// last wins, thus the order in which nodes are added is significant.
- $this->nodes[] = array(
+ $this->nodes[] = [
'object' => $node,
'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT,
- );
+ ];
$this->reset();
}
@@ -131,7 +130,7 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
throw new EmptyRingException('Cannot initialize an empty hashring.');
}
- $this->ring = array();
+ $this->ring = [];
$totalWeight = $this->computeTotalWeight();
$nodesCount = count($this->nodes);
@@ -161,7 +160,7 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
$replicas = (int) round($weightRatio * $totalNodes * $replicas);
for ($i = 0; $i < $replicas; ++$i) {
- $key = crc32("$nodeHash:$i");
+ $key = $this->hash("$nodeHash:$i");
$ring[$key] = $nodeObject;
}
}
@@ -239,9 +238,8 @@ class HashRing implements DistributorInterface, HashGeneratorInterface
public function get($value)
{
$hash = $this->hash($value);
- $node = $this->getByHash($hash);
- return $node;
+ return $this->getByHash($hash);
}
/**
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php b/plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php
similarity index 92%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php
rename to plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php
index dc77f320f..af3b88455 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php
+++ b/plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -15,13 +16,11 @@ namespace Predis\Cluster\Distributor;
* This class implements an hashring-based distributor that uses the same
* algorithm of libketama to distribute keys in a cluster using client-side
* sharding.
- *
- * @author Daniele Alessandri
* @author Lorenzo Castelli
*/
class KetamaRing extends HashRing
{
- const DEFAULT_REPLICAS = 160;
+ public const DEFAULT_REPLICAS = 160;
/**
* @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/CRC16.php b/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php
similarity index 95%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/CRC16.php
rename to plugins/cache-redis/Predis/Cluster/Hash/CRC16.php
index 3add0cef2..4b21d5d21 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/CRC16.php
+++ b/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,12 +14,10 @@ namespace Predis\Cluster\Hash;
/**
* Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster.
- *
- * @author Daniele Alessandri
*/
class CRC16 implements HashGeneratorInterface
{
- private static $CCITT_16 = array(
+ private static $CCITT_16 = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
@@ -51,7 +50,7 @@ class CRC16 implements HashGeneratorInterface
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
- );
+ ];
/**
* {@inheritdoc}
@@ -61,6 +60,8 @@ class CRC16 implements HashGeneratorInterface
// CRC-CCITT-16 algorithm
$crc = 0;
$CCITT_16 = self::$CCITT_16;
+
+ $value = (string) $value;
$strlen = strlen($value);
for ($i = 0; $i < $strlen; ++$i) {
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php b/plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php
similarity index 84%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php
rename to plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php
index 271b9e720..c835c0e66 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php
+++ b/plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -14,8 +15,6 @@ namespace Predis\Cluster\Hash;
/**
* An hash generator implements the logic used to calculate the hash of a key to
* distribute operations among Redis nodes.
- *
- * @author Daniele Alessandri
*/
interface HashGeneratorInterface
{
diff --git a/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php
new file mode 100644
index 000000000..04f58a0ec
--- /dev/null
+++ b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php
@@ -0,0 +1,42 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -16,8 +17,6 @@ use Predis\Cluster\Distributor\HashRing;
/**
* Default cluster strategy used by Predis to handle client-side sharding.
- *
- * @author Daniele Alessandri
*/
class PredisStrategy extends ClusterStrategy
{
@@ -40,9 +39,8 @@ class PredisStrategy extends ClusterStrategy
{
$key = $this->extractKeyTag($key);
$hash = $this->distributor->hash($key);
- $slot = $this->distributor->getSlot($hash);
- return $slot;
+ return $this->distributor->getSlot($hash);
}
/**
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php b/plugins/cache-redis/Predis/Cluster/RedisStrategy.php
similarity index 76%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php
rename to plugins/cache-redis/Predis/Cluster/RedisStrategy.php
index df0bdb49b..8ae5c0f5e 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php
+++ b/plugins/cache-redis/Predis/Cluster/RedisStrategy.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -18,8 +19,6 @@ use Predis\NotSupportedException;
/**
* Default class used by Predis to calculate hashes out of keys of
* commands supported by redis-cluster.
- *
- * @author Daniele Alessandri
*/
class RedisStrategy extends ClusterStrategy
{
@@ -41,9 +40,8 @@ class RedisStrategy extends ClusterStrategy
public function getSlotByKey($key)
{
$key = $this->extractKeyTag($key);
- $slot = $this->hashGenerator->hash($key) & 0x3FFF;
- return $slot;
+ return $this->hashGenerator->hash($key) & 0x3FFF;
}
/**
@@ -51,8 +49,7 @@ class RedisStrategy extends ClusterStrategy
*/
public function getDistributor()
{
- throw new NotSupportedException(
- 'This cluster strategy does not provide an external distributor'
- );
+ $class = get_class($this);
+ throw new NotSupportedException("$class does not provide an external distributor");
}
}
diff --git a/plugins/cache-redis/Predis/Cluster/SlotMap.php b/plugins/cache-redis/Predis/Cluster/SlotMap.php
new file mode 100644
index 000000000..1af63077a
--- /dev/null
+++ b/plugins/cache-redis/Predis/Cluster/SlotMap.php
@@ -0,0 +1,209 @@
+= 0x0000 && $slot <= 0x3FFF;
+ }
+
+ /**
+ * Checks if the given slot range is valid.
+ *
+ * @param int $first Initial slot of the range.
+ * @param int $last Last slot of the range.
+ *
+ * @return bool
+ */
+ public static function isValidRange($first, $last)
+ {
+ return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last;
+ }
+
+ /**
+ * Resets the slot map.
+ */
+ public function reset()
+ {
+ $this->slots = [];
+ }
+
+ /**
+ * Checks if the slot map is empty.
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return empty($this->slots);
+ }
+
+ /**
+ * Returns the current slot map as a dictionary of $slot => $node.
+ *
+ * The order of the slots in the dictionary is not guaranteed.
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->slots;
+ }
+
+ /**
+ * Returns the list of unique nodes in the slot map.
+ *
+ * @return array
+ */
+ public function getNodes()
+ {
+ return array_keys(array_flip($this->slots));
+ }
+
+ /**
+ * Assigns the specified slot range to a node.
+ *
+ * @param int $first Initial slot of the range.
+ * @param int $last Last slot of the range.
+ * @param NodeConnectionInterface|string $connection ID or connection instance.
+ *
+ * @throws OutOfBoundsException
+ */
+ public function setSlots($first, $last, $connection)
+ {
+ if (!static::isValidRange($first, $last)) {
+ throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`");
+ }
+
+ $this->slots += array_fill($first, $last - $first + 1, (string) $connection);
+ }
+
+ /**
+ * Returns the specified slot range.
+ *
+ * @param int $first Initial slot of the range.
+ * @param int $last Last slot of the range.
+ *
+ * @return array
+ */
+ public function getSlots($first, $last)
+ {
+ if (!static::isValidRange($first, $last)) {
+ throw new OutOfBoundsException("Invalid slot range $first-$last");
+ }
+
+ return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null));
+ }
+
+ /**
+ * Checks if the specified slot is assigned.
+ *
+ * @param int $slot Slot index.
+ *
+ * @return bool
+ */
+ #[ReturnTypeWillChange]
+ public function offsetExists($slot)
+ {
+ return isset($this->slots[$slot]);
+ }
+
+ /**
+ * Returns the node assigned to the specified slot.
+ *
+ * @param int $slot Slot index.
+ *
+ * @return string|null
+ */
+ #[ReturnTypeWillChange]
+ public function offsetGet($slot)
+ {
+ return $this->slots[$slot] ?? null;
+ }
+
+ /**
+ * Assigns the specified slot to a node.
+ *
+ * @param int $slot Slot index.
+ * @param NodeConnectionInterface|string $connection ID or connection instance.
+ *
+ * @return void
+ */
+ #[ReturnTypeWillChange]
+ public function offsetSet($slot, $connection)
+ {
+ if (!static::isValid($slot)) {
+ throw new OutOfBoundsException("Invalid slot $slot for `$connection`");
+ }
+
+ $this->slots[(int) $slot] = (string) $connection;
+ }
+
+ /**
+ * Returns the node assigned to the specified slot.
+ *
+ * @param int $slot Slot index.
+ *
+ * @return void
+ */
+ #[ReturnTypeWillChange]
+ public function offsetUnset($slot)
+ {
+ unset($this->slots[$slot]);
+ }
+
+ /**
+ * Returns the current number of assigned slots.
+ *
+ * @return int
+ */
+ #[ReturnTypeWillChange]
+ public function count()
+ {
+ return count($this->slots);
+ }
+
+ /**
+ * Returns an iterator over the slot map.
+ *
+ * @return Traversable
+ */
+ #[ReturnTypeWillChange]
+ public function getIterator()
+ {
+ return new ArrayIterator($this->slots);
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php b/plugins/cache-redis/Predis/Cluster/StrategyInterface.php
similarity index 89%
rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php
rename to plugins/cache-redis/Predis/Cluster/StrategyInterface.php
index cdf7d09fa..83801ae6f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php
+++ b/plugins/cache-redis/Predis/Cluster/StrategyInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -19,8 +20,6 @@ use Predis\Command\CommandInterface;
* keys extracted from supported commands.
*
* This is mostly useful to support clustering via client-side sharding.
- *
- * @author Daniele Alessandri
*/
interface StrategyInterface
{
@@ -30,7 +29,7 @@ interface StrategyInterface
*
* @param CommandInterface $command Command instance.
*
- * @return int
+ * @return int|null
*/
public function getSlot(CommandInterface $command);
@@ -40,7 +39,7 @@ interface StrategyInterface
*
* @param string $key Key string.
*
- * @return int
+ * @return int|null
*/
public function getSlotByKey($key);
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php b/plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php
similarity index 83%
rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php
rename to plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php
index 922883f05..946bbc3a0 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php
+++ b/plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,8 +12,10 @@
namespace Predis\Collection\Iterator;
+use Iterator;
use Predis\ClientInterface;
use Predis\NotSupportedException;
+use ReturnTypeWillChange;
/**
* Provides the base implementation for a fully-rewindable PHP iterator that can
@@ -24,10 +27,8 @@ use Predis\NotSupportedException;
* can change several times during the iteration process.
*
* @see http://redis.io/commands/scan
- *
- * @author Daniele Alessandri
*/
-abstract class CursorBasedIterator implements \Iterator
+abstract class CursorBasedIterator implements Iterator
{
protected $client;
protected $match;
@@ -65,8 +66,8 @@ abstract class CursorBasedIterator implements \Iterator
*/
protected function requiredCommand(ClientInterface $client, $commandID)
{
- if (!$client->getProfile()->supportsCommand($commandID)) {
- throw new NotSupportedException("The current profile does not support '$commandID'.");
+ if (!$client->getCommandFactory()->supports($commandID)) {
+ throw new NotSupportedException("'$commandID' is not supported by the current command factory.");
}
}
@@ -77,7 +78,7 @@ abstract class CursorBasedIterator implements \Iterator
{
$this->valid = true;
$this->fetchmore = true;
- $this->elements = array();
+ $this->elements = [];
$this->cursor = 0;
$this->position = -1;
$this->current = null;
@@ -90,9 +91,9 @@ abstract class CursorBasedIterator implements \Iterator
*/
protected function getScanOptions()
{
- $options = array();
+ $options = [];
- if (strlen($this->match) > 0) {
+ if (strlen(strval($this->match)) > 0) {
$options['MATCH'] = $this->match;
}
@@ -117,7 +118,7 @@ abstract class CursorBasedIterator implements \Iterator
*/
protected function fetch()
{
- list($cursor, $elements) = $this->executeCommand();
+ [$cursor, $elements] = $this->executeCommand();
if (!$cursor) {
$this->fetchmore = false;
@@ -137,8 +138,9 @@ abstract class CursorBasedIterator implements \Iterator
}
/**
- * {@inheritdoc}
+ * @return void
*/
+ #[ReturnTypeWillChange]
public function rewind()
{
$this->reset();
@@ -146,27 +148,30 @@ abstract class CursorBasedIterator implements \Iterator
}
/**
- * {@inheritdoc}
+ * @return mixed
*/
+ #[ReturnTypeWillChange]
public function current()
{
return $this->current;
}
/**
- * {@inheritdoc}
+ * @return int|null
*/
+ #[ReturnTypeWillChange]
public function key()
{
return $this->position;
}
/**
- * {@inheritdoc}
+ * @return void
*/
+ #[ReturnTypeWillChange]
public function next()
{
- tryFetch: {
+ tryFetch:
if (!$this->elements && $this->fetchmore) {
$this->fetch();
}
@@ -178,12 +183,12 @@ abstract class CursorBasedIterator implements \Iterator
} else {
$this->valid = false;
}
- }
}
/**
- * {@inheritdoc}
+ * @return bool
*/
+ #[ReturnTypeWillChange]
public function valid()
{
return $this->valid;
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php b/plugins/cache-redis/Predis/Collection/Iterator/HashKey.php
similarity index 84%
rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php
rename to plugins/cache-redis/Predis/Collection/Iterator/HashKey.php
index aa8aeaf02..91b7d27ff 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php
+++ b/plugins/cache-redis/Predis/Collection/Iterator/HashKey.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,9 +18,7 @@ use Predis\ClientInterface;
* Abstracts the iteration of fields and values of an hash by leveraging the
* HSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
*
- * @author Daniele Alessandri
- *
- * @link http://redis.io/commands/scan
+ * @see http://redis.io/commands/scan
*/
class HashKey extends CursorBasedIterator
{
@@ -51,6 +50,8 @@ class HashKey extends CursorBasedIterator
protected function extractNext()
{
$this->position = key($this->elements);
- $this->current = array_shift($this->elements);
+ $this->current = current($this->elements);
+
+ unset($this->elements[$this->position]);
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php b/plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php
similarity index 85%
rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php
rename to plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php
index 5d985b9bc..b5fa022aa 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php
+++ b/plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,9 +18,7 @@ use Predis\ClientInterface;
* Abstracts the iteration of the keyspace on a Redis instance by leveraging the
* SCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
*
- * @author Daniele Alessandri
- *
- * @link http://redis.io/commands/scan
+ * @see http://redis.io/commands/scan
*/
class Keyspace extends CursorBasedIterator
{
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php b/plugins/cache-redis/Predis/Collection/Iterator/ListKey.php
similarity index 82%
rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php
rename to plugins/cache-redis/Predis/Collection/Iterator/ListKey.php
index 7a6eb479e..79ab3aa19 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php
+++ b/plugins/cache-redis/Predis/Collection/Iterator/ListKey.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,8 +12,11 @@
namespace Predis\Collection\Iterator;
+use InvalidArgumentException;
+use Iterator;
use Predis\ClientInterface;
use Predis\NotSupportedException;
+use ReturnTypeWillChange;
/**
* Abstracts the iteration of items stored in a list by leveraging the LRANGE
@@ -24,11 +28,9 @@ use Predis\NotSupportedException;
* guarantees on the returned elements because the collection can change several
* times (trimmed, deleted, overwritten) during the iteration process.
*
- * @author Daniele Alessandri
- *
- * @link http://redis.io/commands/lrange
+ * @see http://redis.io/commands/lrange
*/
-class ListKey implements \Iterator
+class ListKey implements Iterator
{
protected $client;
protected $count;
@@ -45,14 +47,14 @@ class ListKey implements \Iterator
* @param string $key Redis list key.
* @param int $count Number of items retrieved on each fetch operation.
*
- * @throws \InvalidArgumentException
+ * @throws InvalidArgumentException
*/
public function __construct(ClientInterface $client, $key, $count = 10)
{
$this->requiredCommand($client, 'LRANGE');
if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) {
- throw new \InvalidArgumentException('The $count argument must be a positive integer.');
+ throw new InvalidArgumentException('The $count argument must be a positive integer.');
}
$this->client = $client;
@@ -73,8 +75,8 @@ class ListKey implements \Iterator
*/
protected function requiredCommand(ClientInterface $client, $commandID)
{
- if (!$client->getProfile()->supportsCommand($commandID)) {
- throw new NotSupportedException("The current profile does not support '$commandID'.");
+ if (!$client->getCommandFactory()->supports($commandID)) {
+ throw new NotSupportedException("'$commandID' is not supported by the current command factory.");
}
}
@@ -85,7 +87,7 @@ class ListKey implements \Iterator
{
$this->valid = true;
$this->fetchmore = true;
- $this->elements = array();
+ $this->elements = [];
$this->position = -1;
$this->current = null;
}
@@ -126,8 +128,9 @@ class ListKey implements \Iterator
}
/**
- * {@inheritdoc}
+ * @return void
*/
+ #[ReturnTypeWillChange]
public function rewind()
{
$this->reset();
@@ -135,24 +138,27 @@ class ListKey implements \Iterator
}
/**
- * {@inheritdoc}
+ * @return mixed
*/
+ #[ReturnTypeWillChange]
public function current()
{
return $this->current;
}
/**
- * {@inheritdoc}
+ * @return int|null
*/
+ #[ReturnTypeWillChange]
public function key()
{
return $this->position;
}
/**
- * {@inheritdoc}
+ * @return void
*/
+ #[ReturnTypeWillChange]
public function next()
{
if (!$this->elements && $this->fetchmore) {
@@ -167,8 +173,9 @@ class ListKey implements \Iterator
}
/**
- * {@inheritdoc}
+ * @return bool
*/
+ #[ReturnTypeWillChange]
public function valid()
{
return $this->valid;
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php b/plugins/cache-redis/Predis/Collection/Iterator/SetKey.php
similarity index 86%
rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php
rename to plugins/cache-redis/Predis/Collection/Iterator/SetKey.php
index bf2543975..74f982346 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php
+++ b/plugins/cache-redis/Predis/Collection/Iterator/SetKey.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,9 +18,7 @@ use Predis\ClientInterface;
* Abstracts the iteration of members stored in a set by leveraging the SSCAN
* command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
*
- * @author Daniele Alessandri
- *
- * @link http://redis.io/commands/scan
+ * @see http://redis.io/commands/scan
*/
class SetKey extends CursorBasedIterator
{
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php b/plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php
similarity index 76%
rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php
rename to plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php
index e2f178922..abee8c252 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php
+++ b/plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -17,9 +18,7 @@ use Predis\ClientInterface;
* Abstracts the iteration of members stored in a sorted set by leveraging the
* ZSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
*
- * @author Daniele Alessandri
- *
- * @link http://redis.io/commands/scan
+ * @see http://redis.io/commands/scan
*/
class SortedSetKey extends CursorBasedIterator
{
@@ -50,11 +49,9 @@ class SortedSetKey extends CursorBasedIterator
*/
protected function extractNext()
{
- if ($kv = each($this->elements)) {
- $this->position = $kv[0];
- $this->current = $kv[1];
+ $this->position = key($this->elements);
+ $this->current = current($this->elements);
- unset($this->elements[$this->position]);
- }
+ unset($this->elements[$this->position]);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php
new file mode 100644
index 000000000..11073c054
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php
@@ -0,0 +1,26 @@
+unit = $unit;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php
new file mode 100644
index 000000000..7dd9f23b7
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php
@@ -0,0 +1,43 @@
+width = $width;
+ $this->height = $height;
+ $this->setUnit($unit);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return [self::KEYWORD, $this->width, $this->height, $this->unit];
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php
new file mode 100644
index 000000000..767886cef
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php
@@ -0,0 +1,19 @@
+radius = $radius;
+ $this->setUnit($unit);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return [self::KEYWORD, $this->radius, $this->unit];
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php
new file mode 100644
index 000000000..44700bda4
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php
@@ -0,0 +1,19 @@
+longitude = $longitude;
+ $this->latitude = $latitude;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return [self::KEYWORD, $this->longitude, $this->latitude];
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php
new file mode 100644
index 000000000..9e24b2ba0
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php
@@ -0,0 +1,36 @@
+member = $member;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return [self::KEYWORD, $this->member];
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php
new file mode 100644
index 000000000..950967070
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php
@@ -0,0 +1,161 @@
+ 'ASC',
+ 'desc' => 'DESC',
+ ];
+
+ /**
+ * Loads document attributes from the source document.
+ *
+ * @param string ...$fields Could be just '*' to load all fields
+ * @return $this
+ */
+ public function load(string ...$fields): self
+ {
+ $arguments = func_get_args();
+
+ $this->arguments[] = 'LOAD';
+
+ if ($arguments[0] === '*') {
+ $this->arguments[] = '*';
+
+ return $this;
+ }
+
+ $this->arguments[] = count($arguments);
+ $this->arguments = array_merge($this->arguments, $arguments);
+
+ return $this;
+ }
+
+ /**
+ * Loads document attributes from the source document.
+ *
+ * @param string ...$properties
+ * @return $this
+ */
+ public function groupBy(string ...$properties): self
+ {
+ $arguments = func_get_args();
+
+ array_push($this->arguments, 'GROUPBY', count($arguments));
+ $this->arguments = array_merge($this->arguments, $arguments);
+
+ return $this;
+ }
+
+ /**
+ * Groups the results in the pipeline based on one or more properties.
+ *
+ * If you want to add alias property to your argument just add "true" value in arguments enumeration,
+ * next value will be considered as alias to previous one.
+ *
+ * Example: 'argument', true, 'name' => 'argument' AS 'name'
+ *
+ * @param string $function
+ * @param string|bool ...$argument
+ * @return $this
+ */
+ public function reduce(string $function, ...$argument): self
+ {
+ $arguments = func_get_args();
+ $functionValue = array_shift($arguments);
+ $argumentsCounter = 0;
+
+ for ($i = 0, $iMax = count($arguments); $i < $iMax; $i++) {
+ if (true === $arguments[$i]) {
+ $arguments[$i] = 'AS';
+ $i++;
+ continue;
+ }
+
+ $argumentsCounter++;
+ }
+
+ array_push($this->arguments, 'REDUCE', $functionValue);
+ $this->arguments = array_merge($this->arguments, [$argumentsCounter], $arguments);
+
+ return $this;
+ }
+
+ /**
+ * Sorts the pipeline up until the point of SORTBY, using a list of properties.
+ *
+ * @param int $max
+ * @param string ...$properties Enumeration of properties, including sorting direction (ASC, DESC)
+ * @return $this
+ */
+ public function sortBy(int $max = 0, ...$properties): self
+ {
+ $arguments = func_get_args();
+ $maxValue = array_shift($arguments);
+
+ $this->arguments[] = 'SORTBY';
+ $this->arguments = array_merge($this->arguments, [count($arguments)], $arguments);
+
+ if ($maxValue !== 0) {
+ array_push($this->arguments, 'MAX', $maxValue);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Applies a 1-to-1 transformation on one or more properties and either stores the result
+ * as a new property down the pipeline or replaces any property using this transformation.
+ *
+ * @param string $expression
+ * @param string $as
+ * @return $this
+ */
+ public function apply(string $expression, string $as = ''): self
+ {
+ array_push($this->arguments, 'APPLY', $expression);
+
+ if ($as !== '') {
+ array_push($this->arguments, 'AS', $as);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Scan part of the results with a quicker alternative than LIMIT.
+ *
+ * @param int $readSize
+ * @param int $idleTime
+ * @return $this
+ */
+ public function withCursor(int $readSize = 0, int $idleTime = 0): self
+ {
+ $this->arguments[] = 'WITHCURSOR';
+
+ if ($readSize !== 0) {
+ array_push($this->arguments, 'COUNT', $readSize);
+ }
+
+ if ($idleTime !== 0) {
+ array_push($this->arguments, 'MAXIDLE', $idleTime);
+ }
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php
new file mode 100644
index 000000000..5acd2fe1e
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php
@@ -0,0 +1,17 @@
+arguments[] = 'LANGUAGE';
+ $this->arguments[] = $defaultLanguage;
+
+ return $this;
+ }
+
+ /**
+ * Selects the dialect version under which to execute the query.
+ * If not specified, the query will execute under the default dialect version
+ * set during module initial loading or via FT.CONFIG SET command.
+ *
+ * @param string $dialect
+ * @return $this
+ */
+ public function dialect(string $dialect): self
+ {
+ $this->arguments[] = 'DIALECT';
+ $this->arguments[] = $dialect;
+
+ return $this;
+ }
+
+ /**
+ * If set, does not scan and index.
+ *
+ * @return $this
+ */
+ public function skipInitialScan(): self
+ {
+ $this->arguments[] = 'SKIPINITIALSCAN';
+
+ return $this;
+ }
+
+ /**
+ * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
+ *
+ * @param string $payload
+ * @return $this
+ */
+ public function payload(string $payload): self
+ {
+ $this->arguments[] = 'PAYLOAD';
+ $this->arguments[] = $payload;
+
+ return $this;
+ }
+
+ /**
+ * Also returns the relative internal score of each document.
+ *
+ * @return $this
+ */
+ public function withScores(): self
+ {
+ $this->arguments[] = 'WITHSCORES';
+
+ return $this;
+ }
+
+ /**
+ * Retrieves optional document payloads.
+ *
+ * @return $this
+ */
+ public function withPayloads(): self
+ {
+ $this->arguments[] = 'WITHPAYLOADS';
+
+ return $this;
+ }
+
+ /**
+ * Does not try to use stemming for query expansion but searches the query terms verbatim.
+ *
+ * @return $this
+ */
+ public function verbatim(): self
+ {
+ $this->arguments[] = 'VERBATIM';
+
+ return $this;
+ }
+
+ /**
+ * Overrides the timeout parameter of the module.
+ *
+ * @param int $timeout
+ * @return $this
+ */
+ public function timeout(int $timeout): self
+ {
+ $this->arguments[] = 'TIMEOUT';
+ $this->arguments[] = $timeout;
+
+ return $this;
+ }
+
+ /**
+ * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
+ *
+ * @param int $offset
+ * @param int $num
+ * @return $this
+ */
+ public function limit(int $offset, int $num): self
+ {
+ array_push($this->arguments, 'LIMIT', $offset, $num);
+
+ return $this;
+ }
+
+ /**
+ * Adds filter expression into index.
+ *
+ * @param string $filter
+ * @return $this
+ */
+ public function filter(string $filter): self
+ {
+ $this->arguments[] = 'FILTER';
+ $this->arguments[] = $filter;
+
+ return $this;
+ }
+
+ /**
+ * Defines one or more value parameters. Each parameter has a name and a value.
+ *
+ * Example: ['name1', 'value1', 'name2', 'value2'...]
+ *
+ * @param array $nameValuesDictionary
+ * @return $this
+ */
+ public function params(array $nameValuesDictionary): self
+ {
+ $this->arguments[] = 'PARAMS';
+ $this->arguments[] = count($nameValuesDictionary);
+ $this->arguments = array_merge($this->arguments, $nameValuesDictionary);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php
new file mode 100644
index 000000000..b8e0176ba
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php
@@ -0,0 +1,191 @@
+ 'HASH',
+ 'json' => 'JSON',
+ ];
+
+ /**
+ * Specify data type for given index. To index JSON you must have the RedisJSON module to be installed.
+ *
+ * @param string $modifier
+ * @return $this
+ */
+ public function on(string $modifier = 'HASH'): self
+ {
+ if (in_array(strtoupper($modifier), $this->supportedDataTypesEnum)) {
+ $this->arguments[] = 'ON';
+ $this->arguments[] = $this->supportedDataTypesEnum[strtolower($modifier)];
+
+ return $this;
+ }
+
+ $enumValues = implode(', ', array_values($this->supportedDataTypesEnum));
+ throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}");
+ }
+
+ /**
+ * Adds one or more prefixes into index.
+ *
+ * @param array $prefixes
+ * @return $this
+ */
+ public function prefix(array $prefixes): self
+ {
+ $this->arguments[] = 'PREFIX';
+ $this->arguments[] = count($prefixes);
+ $this->arguments = array_merge($this->arguments, $prefixes);
+
+ return $this;
+ }
+
+ /**
+ * Document attribute set as document language.
+ *
+ * @param string $languageAttribute
+ * @return $this
+ */
+ public function languageField(string $languageAttribute): self
+ {
+ $this->arguments[] = 'LANGUAGE_FIELD';
+ $this->arguments[] = $languageAttribute;
+
+ return $this;
+ }
+
+ /**
+ * Default score for documents in the index.
+ *
+ * @param float $defaultScore
+ * @return $this
+ */
+ public function score(float $defaultScore = 1.0): self
+ {
+ $this->arguments[] = 'SCORE';
+ $this->arguments[] = $defaultScore;
+
+ return $this;
+ }
+
+ /**
+ * Document attribute that used as the document rank based on the user ranking.
+ *
+ * @param string $scoreAttribute
+ * @return $this
+ */
+ public function scoreField(string $scoreAttribute): self
+ {
+ $this->arguments[] = 'SCORE_FIELD';
+ $this->arguments[] = $scoreAttribute;
+
+ return $this;
+ }
+
+ /**
+ * Forces RediSearch to encode indexes as if there were more than 32 text attributes.
+ *
+ * @return $this
+ */
+ public function maxTextFields(): self
+ {
+ $this->arguments[] = 'MAXTEXTFIELDS';
+
+ return $this;
+ }
+
+ /**
+ * Does not store term offsets for documents.
+ *
+ * @return $this
+ */
+ public function noOffsets(): self
+ {
+ $this->arguments[] = 'NOOFFSETS';
+
+ return $this;
+ }
+
+ /**
+ * Creates a lightweight temporary index that expires after a specified period of inactivity, in seconds.
+ *
+ * @param int $seconds
+ * @return $this
+ */
+ public function temporary(int $seconds): self
+ {
+ $this->arguments[] = 'TEMPORARY';
+ $this->arguments[] = $seconds;
+
+ return $this;
+ }
+
+ /**
+ * Conserves storage space and memory by disabling highlighting support.
+ *
+ * @return $this
+ */
+ public function noHl(): self
+ {
+ $this->arguments[] = 'NOHL';
+
+ return $this;
+ }
+
+ /**
+ * Does not store attribute bits for each term.
+ *
+ * @return $this
+ */
+ public function noFields(): self
+ {
+ $this->arguments[] = 'NOFIELDS';
+
+ return $this;
+ }
+
+ /**
+ * Avoids saving the term frequencies in the index.
+ *
+ * @return $this
+ */
+ public function noFreqs(): self
+ {
+ $this->arguments[] = 'NOFREQS';
+
+ return $this;
+ }
+
+ /**
+ * Sets the index with a custom stopword list, to be ignored during indexing and search time.
+ *
+ * @param array $stopWords
+ * @return $this
+ */
+ public function stopWords(array $stopWords): self
+ {
+ $this->arguments[] = 'STOPWORDS';
+ $this->arguments[] = count($stopWords);
+ $this->arguments = array_merge($this->arguments, $stopWords);
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php
new file mode 100644
index 000000000..a8bd6b56f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php
@@ -0,0 +1,44 @@
+arguments, 'COUNT', $readSize);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php
new file mode 100644
index 000000000..0c6313201
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php
@@ -0,0 +1,43 @@
+arguments[] = 'DD';
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php
new file mode 100644
index 000000000..b4bd235b9
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php
@@ -0,0 +1,17 @@
+arguments[] = 'SEARCH';
+
+ return $this;
+ }
+
+ /**
+ * Adds aggregate context.
+ *
+ * @return $this
+ */
+ public function aggregate(): self
+ {
+ $this->arguments[] = 'AGGREGATE';
+
+ return $this;
+ }
+
+ /**
+ * Removes details of reader iterator.
+ *
+ * @return $this
+ */
+ public function limited(): self
+ {
+ $this->arguments[] = 'LIMITED';
+
+ return $this;
+ }
+
+ /**
+ * Is query string, as if sent to FT.SEARCH.
+ *
+ * @param string $query
+ * @return $this
+ */
+ public function query(string $query): self
+ {
+ $this->arguments[] = 'QUERY';
+ $this->arguments[] = $query;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php
new file mode 100644
index 000000000..eb49f0995
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php
@@ -0,0 +1,69 @@
+fieldArguments[] = $identifier;
+
+ if ($alias !== '') {
+ $this->fieldArguments[] = 'AS';
+ $this->fieldArguments[] = $alias;
+ }
+
+ $this->fieldArguments[] = $fieldType;
+
+ if ($sortable === self::SORTABLE) {
+ $this->fieldArguments[] = 'SORTABLE';
+ } elseif ($sortable === self::SORTABLE_UNF) {
+ $this->fieldArguments[] = 'SORTABLE';
+ $this->fieldArguments[] = 'UNF';
+ }
+
+ if ($noIndex) {
+ $this->fieldArguments[] = 'NOINDEX';
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->fieldArguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php
new file mode 100644
index 000000000..80e57eba0
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php
@@ -0,0 +1,22 @@
+setCommonOptions('GEO', $identifier, $alias, $sortable, $noIndex);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php
new file mode 100644
index 000000000..758b4e9c6
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php
@@ -0,0 +1,31 @@
+setCommonOptions('NUMERIC', $identifier, $alias, $sortable, $noIndex);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php
new file mode 100644
index 000000000..358b3090e
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php
@@ -0,0 +1,44 @@
+setCommonOptions('TAG', $identifier, $alias, $sortable, $noIndex);
+
+ if ($separator !== ',') {
+ $this->fieldArguments[] = 'SEPARATOR';
+ $this->fieldArguments[] = $separator;
+ }
+
+ if ($caseSensitive) {
+ $this->fieldArguments[] = 'CASESENSITIVE';
+ }
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php
new file mode 100644
index 000000000..d72c62383
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php
@@ -0,0 +1,57 @@
+setCommonOptions('TEXT', $identifier, $alias, $sortable, $noIndex);
+
+ if ($noStem) {
+ $this->fieldArguments[] = 'NOSTEM';
+ }
+
+ if ($phonetic !== '') {
+ $this->fieldArguments[] = 'PHONETIC';
+ $this->fieldArguments[] = $phonetic;
+ }
+
+ if ($weight !== 1) {
+ $this->fieldArguments[] = 'WEIGHT';
+ $this->fieldArguments[] = $weight;
+ }
+
+ if ($withSuffixTrie) {
+ $this->fieldArguments[] = 'WITHSUFFIXTRIE';
+ }
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php
new file mode 100644
index 000000000..5228c2250
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php
@@ -0,0 +1,47 @@
+setCommonOptions('VECTOR', $fieldName, $alias);
+
+ array_push($this->fieldArguments, $algorithm, count($attributeNameValueDictionary));
+ $this->fieldArguments = array_merge($this->fieldArguments, $attributeNameValueDictionary);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->fieldArguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php
new file mode 100644
index 000000000..d1eb70580
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php
@@ -0,0 +1,306 @@
+ 'ASC',
+ 'desc' => 'DESC',
+ ];
+
+ /**
+ * Returns the document ids and not the content.
+ *
+ * @return $this
+ */
+ public function noContent(): self
+ {
+ $this->arguments[] = 'NOCONTENT';
+
+ return $this;
+ }
+
+ /**
+ * Returns the value of the sorting key, right after the id and score and/or payload, if requested.
+ *
+ * @return $this
+ */
+ public function withSortKeys(): self
+ {
+ $this->arguments[] = 'WITHSORTKEYS';
+
+ return $this;
+ }
+
+ /**
+ * Limits results to those having numeric values ranging between min and max,
+ * if numeric_attribute is defined as a numeric attribute in FT.CREATE.
+ * Min and max follow ZRANGE syntax, and can be -inf, +inf, and use( for exclusive ranges.
+ * Multiple numeric filters for different attributes are supported in one query.
+ *
+ * @param array ...$filter Should contain: numeric_field, min and max. Example: ['numeric_field', 1, 10]
+ * @return $this
+ */
+ public function searchFilter(array ...$filter): self
+ {
+ $arguments = func_get_args();
+
+ foreach ($arguments as $argument) {
+ array_push($this->arguments, 'FILTER', ...$argument);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter the results to a given radius from lon and lat. Radius is given as a number and units.
+ *
+ * @param array ...$filter Should contain: geo_field, lon, lat, radius, unit. Example: ['geo_field', 34.1231, 35.1231, 300, km]
+ * @return $this
+ */
+ public function geoFilter(array ...$filter): self
+ {
+ $arguments = func_get_args();
+
+ foreach ($arguments as $argument) {
+ array_push($this->arguments, 'GEOFILTER', ...$argument);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Limits the result to a given set of keys specified in the list.
+ *
+ * @param array $keys
+ * @return $this
+ */
+ public function inKeys(array $keys): self
+ {
+ $this->arguments[] = 'INKEYS';
+ $this->arguments[] = count($keys);
+ $this->arguments = array_merge($this->arguments, $keys);
+
+ return $this;
+ }
+
+ /**
+ * Filters the results to those appearing only in specific attributes of the document, like title or URL.
+ *
+ * @param array $fields
+ * @return $this
+ */
+ public function inFields(array $fields): self
+ {
+ $this->arguments[] = 'INFIELDS';
+ $this->arguments[] = count($fields);
+ $this->arguments = array_merge($this->arguments, $fields);
+
+ return $this;
+ }
+
+ /**
+ * Limits the attributes returned from the document.
+ * Num is the number of attributes following the keyword.
+ * If num is 0, it acts like NOCONTENT.
+ * Identifier is either an attribute name (for hashes and JSON) or a JSON Path expression (for JSON).
+ * Property is an optional name used in the result. If not provided, the identifier is used in the result.
+ *
+ * If you want to add alias property to your identifier just add "true" value in identifier enumeration,
+ * next value will be considered as alias to previous one.
+ *
+ * Example: 'identifier', true, 'property' => 'identifier' AS 'property'
+ *
+ * @param int $count
+ * @param string|bool ...$identifier
+ * @return $this
+ */
+ public function addReturn(int $count, ...$identifier): self
+ {
+ $arguments = func_get_args();
+
+ $this->arguments[] = 'RETURN';
+
+ for ($i = 1, $iMax = count($arguments); $i < $iMax; $i++) {
+ if (true === $arguments[$i]) {
+ $arguments[$i] = 'AS';
+ }
+ }
+
+ $this->arguments = array_merge($this->arguments, $arguments);
+
+ return $this;
+ }
+
+ /**
+ * Returns only the sections of the attribute that contain the matched text.
+ *
+ * @param array $fields
+ * @param int $frags
+ * @param int $len
+ * @param string $separator
+ * @return $this
+ */
+ public function summarize(array $fields = [], int $frags = 0, int $len = 0, string $separator = ''): self
+ {
+ $this->arguments[] = 'SUMMARIZE';
+
+ if (!empty($fields)) {
+ $this->arguments[] = 'FIELDS';
+ $this->arguments[] = count($fields);
+ $this->arguments = array_merge($this->arguments, $fields);
+ }
+
+ if ($frags !== 0) {
+ $this->arguments[] = 'FRAGS';
+ $this->arguments[] = $frags;
+ }
+
+ if ($len !== 0) {
+ $this->arguments[] = 'LEN';
+ $this->arguments[] = $len;
+ }
+
+ if ($separator !== '') {
+ $this->arguments[] = 'SEPARATOR';
+ $this->arguments[] = $separator;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Formats occurrences of matched text.
+ *
+ * @param array $fields
+ * @param string $openTag
+ * @param string $closeTag
+ * @return $this
+ */
+ public function highlight(array $fields = [], string $openTag = '', string $closeTag = ''): self
+ {
+ $this->arguments[] = 'HIGHLIGHT';
+
+ if (!empty($fields)) {
+ $this->arguments[] = 'FIELDS';
+ $this->arguments[] = count($fields);
+ $this->arguments = array_merge($this->arguments, $fields);
+ }
+
+ if ($openTag !== '' && $closeTag !== '') {
+ array_push($this->arguments, 'TAGS', $openTag, $closeTag);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows a maximum of N intervening number of unmatched offsets between phrase terms.
+ * In other words, the slop for exact phrases is 0.
+ *
+ * @param int $slop
+ * @return $this
+ */
+ public function slop(int $slop): self
+ {
+ $this->arguments[] = 'SLOP';
+ $this->arguments[] = $slop;
+
+ return $this;
+ }
+
+ /**
+ * Puts the query terms in the same order in the document as in the query, regardless of the offsets between them.
+ * Typically used in conjunction with SLOP.
+ *
+ * @return $this
+ */
+ public function inOrder(): self
+ {
+ $this->arguments[] = 'INORDER';
+
+ return $this;
+ }
+
+ /**
+ * Uses a custom query expander instead of the stemmer.
+ *
+ * @param string $expander
+ * @return $this
+ */
+ public function expander(string $expander): self
+ {
+ $this->arguments[] = 'EXPANDER';
+ $this->arguments[] = $expander;
+
+ return $this;
+ }
+
+ /**
+ * Uses a custom scoring function you define.
+ *
+ * @param string $scorer
+ * @return $this
+ */
+ public function scorer(string $scorer): self
+ {
+ $this->arguments[] = 'SCORER';
+ $this->arguments[] = $scorer;
+
+ return $this;
+ }
+
+ /**
+ * Returns a textual description of how the scores were calculated.
+ * Using this options requires the WITHSCORES option.
+ *
+ * @return $this
+ */
+ public function explainScore(): self
+ {
+ $this->arguments[] = 'EXPLAINSCORE';
+
+ return $this;
+ }
+
+ /**
+ * Orders the results by the value of this attribute.
+ * This applies to both text and numeric attributes.
+ * Attributes needed for SORTBY should be declared as SORTABLE in the index, in order to be available with very low latency.
+ * Note that this adds memory overhead.
+ *
+ * @param string $sortAttribute
+ * @param string $orderBy
+ * @return $this
+ */
+ public function sortBy(string $sortAttribute, string $orderBy = 'asc'): self
+ {
+ $this->arguments[] = 'SORTBY';
+ $this->arguments[] = $sortAttribute;
+
+ if (in_array(strtoupper($orderBy), $this->sortingEnum)) {
+ $this->arguments[] = $this->sortingEnum[strtolower($orderBy)];
+ } else {
+ $enumValues = implode(', ', array_values($this->sortingEnum));
+ throw new InvalidArgumentException("Wrong order direction value given. Currently supports: {$enumValues}");
+ }
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php
new file mode 100644
index 000000000..7a6c24891
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php
@@ -0,0 +1,59 @@
+ 'INCLUDE',
+ 'exclude' => 'EXCLUDE',
+ ];
+
+ /**
+ * Is maximum Levenshtein distance for spelling suggestions (default: 1, max: 4).
+ *
+ * @return $this
+ */
+ public function distance(int $distance): self
+ {
+ $this->arguments[] = 'DISTANCE';
+ $this->arguments[] = $distance;
+
+ return $this;
+ }
+
+ /**
+ * Specifies an inclusion (INCLUDE) or exclusion (EXCLUDE) of a custom dictionary named {dict}.
+ *
+ * @param string $dictionary
+ * @param string $modifier
+ * @param string ...$terms
+ * @return $this
+ */
+ public function terms(string $dictionary, string $modifier = 'INCLUDE', string ...$terms): self
+ {
+ if (!in_array(strtoupper($modifier), $this->termsEnum)) {
+ $enumValues = implode(', ', array_values($this->termsEnum));
+ throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}");
+ }
+
+ array_push($this->arguments, 'TERMS', $this->termsEnum[strtolower($modifier)], $dictionary, ...$terms);
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php
new file mode 100644
index 000000000..c8b976978
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php
@@ -0,0 +1,28 @@
+arguments[] = 'INCR';
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php
new file mode 100644
index 000000000..1176c7736
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php
@@ -0,0 +1,41 @@
+arguments[] = 'FUZZY';
+
+ return $this;
+ }
+
+ /**
+ * Limits the results to a maximum of num (default: 5).
+ *
+ * @param int $num
+ * @return $this
+ */
+ public function max(int $num): self
+ {
+ array_push($this->arguments, 'MAX', $num);
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php
new file mode 100644
index 000000000..a6b286a48
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php
@@ -0,0 +1,17 @@
+offset = $offset;
+ $this->count = $count;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return [self::KEYWORD, $this->offset, $this->count];
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/Server/To.php b/plugins/cache-redis/Predis/Command/Argument/Server/To.php
new file mode 100644
index 000000000..1d77ef681
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/Server/To.php
@@ -0,0 +1,57 @@
+host = $host;
+ $this->port = $port;
+ $this->isForce = $isForce;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ $arguments = [self::KEYWORD, $this->host, $this->port];
+
+ if ($this->isForce) {
+ $arguments[] = self::FORCE_KEYWORD;
+ }
+
+ return $arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php
new file mode 100644
index 000000000..a9fe6f79f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php
@@ -0,0 +1,30 @@
+arguments, 'ON_DUPLICATE', $policy);
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php
new file mode 100644
index 000000000..238ebc36d
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php
@@ -0,0 +1,17 @@
+arguments, 'RETENTION', $retentionPeriod);
+
+ return $this;
+ }
+
+ /**
+ * Is initial allocation size, in bytes, for the data part of each new chunk.
+ *
+ * @param int $size
+ * @return $this
+ */
+ public function chunkSize(int $size): self
+ {
+ array_push($this->arguments, 'CHUNK_SIZE', $size);
+
+ return $this;
+ }
+
+ /**
+ * Is policy for handling insertion of multiple samples with identical timestamps.
+ *
+ * @param string $policy
+ * @return $this
+ */
+ public function duplicatePolicy(string $policy = self::POLICY_BLOCK): self
+ {
+ array_push($this->arguments, 'DUPLICATE_POLICY', $policy);
+
+ return $this;
+ }
+
+ /**
+ * Is set of label-value pairs that represent metadata labels of the key and serve as a secondary index.
+ *
+ * @param mixed ...$labelValuePair
+ * @return $this
+ */
+ public function labels(...$labelValuePair): self
+ {
+ array_push($this->arguments, 'LABELS', ...$labelValuePair);
+
+ return $this;
+ }
+
+ /**
+ * Specifies the series samples encoding format.
+ *
+ * @param string $encoding
+ * @return $this
+ */
+ public function encoding(string $encoding = self::ENCODING_COMPRESSED): self
+ {
+ array_push($this->arguments, 'ENCODING', $encoding);
+
+ return $this;
+ }
+
+ /**
+ * Is used when a time series is a compaction.
+ * With LATEST, TS.GET reports the compacted value of the latest, possibly partial, bucket.
+ *
+ * @return $this
+ */
+ public function latest(): self
+ {
+ $this->arguments[] = 'LATEST';
+
+ return $this;
+ }
+
+ /**
+ * Includes in the reply all label-value pairs representing metadata labels of the time series.
+ *
+ * @return $this
+ */
+ public function withLabels(): self
+ {
+ $this->arguments[] = 'WITHLABELS';
+
+ return $this;
+ }
+
+ /**
+ * Returns a subset of the label-value pairs that represent metadata labels of the time series.
+ *
+ * @return $this
+ */
+ public function selectedLabels(string ...$labels): self
+ {
+ array_push($this->arguments, 'SELECTED_LABELS', ...$labels);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php
new file mode 100644
index 000000000..e47d8ef72
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php
@@ -0,0 +1,17 @@
+arguments, 'TIMESTAMP', $timeStamp);
+
+ return $this;
+ }
+
+ /**
+ * Changes data storage from compressed (default) to uncompressed.
+ *
+ * @return $this
+ */
+ public function uncompressed(): self
+ {
+ $this->arguments[] = 'UNCOMPRESSED';
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php
new file mode 100644
index 000000000..1b2cec664
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php
@@ -0,0 +1,43 @@
+arguments[] = 'DEBUG';
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray(): array
+ {
+ return $this->arguments;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php
new file mode 100644
index 000000000..585f574e6
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php
@@ -0,0 +1,17 @@
+arguments, 'FILTER', ...$filterExpressions);
+
+ return $this;
+ }
+
+ /**
+ * Splits time series into groups, each group contains time series that share the same
+ * value for the provided label name, then aggregates results in each group.
+ *
+ * @param string $label
+ * @param string $reducer
+ * @return $this
+ */
+ public function groupBy(string $label, string $reducer): self
+ {
+ array_push($this->arguments, 'GROUPBY', $label, 'REDUCE', $reducer);
+
+ return $this;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php
new file mode 100644
index 000000000..00d3772d7
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php
@@ -0,0 +1,85 @@
+arguments, 'FILTER_BY_TS', ...$ts);
+
+ return $this;
+ }
+
+ /**
+ * Filters samples by minimum and maximum values.
+ *
+ * @param int $min
+ * @param int $max
+ * @return $this
+ */
+ public function filterByValue(int $min, int $max): self
+ {
+ array_push($this->arguments, 'FILTER_BY_VALUE', $min, $max);
+
+ return $this;
+ }
+
+ /**
+ * Limits the number of returned samples.
+ *
+ * @param int $count
+ * @return $this
+ */
+ public function count(int $count): self
+ {
+ array_push($this->arguments, 'COUNT', $count);
+
+ return $this;
+ }
+
+ /**
+ * Aggregates samples into time buckets.
+ *
+ * @param string $aggregator
+ * @param int $bucketDuration Is duration of each bucket, in milliseconds.
+ * @param int $align It controls the time bucket timestamps by changing the reference timestamp on which a bucket is defined.
+ * @param int $bucketTimestamp Controls how bucket timestamps are reported.
+ * @param bool $empty Is a flag, which, when specified, reports aggregations also for empty buckets.
+ * @return $this
+ */
+ public function aggregation(string $aggregator, int $bucketDuration, int $align = 0, int $bucketTimestamp = 0, bool $empty = false): self
+ {
+ if ($align > 0) {
+ array_push($this->arguments, 'ALIGN', $align);
+ }
+
+ array_push($this->arguments, 'AGGREGATION', $aggregator, $bucketDuration);
+
+ if ($bucketTimestamp > 0) {
+ array_push($this->arguments, 'BUCKETTIMESTAMP', $bucketTimestamp);
+ }
+
+ if (true === $empty) {
+ $this->arguments[] = 'EMPTY';
+ }
+
+ return $this;
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/Command.php b/plugins/cache-redis/Predis/Command/Command.php
similarity index 75%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/Command.php
rename to plugins/cache-redis/Predis/Command/Command.php
index bb538e7c5..68629c454 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/Command.php
+++ b/plugins/cache-redis/Predis/Command/Command.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,32 +14,18 @@ namespace Predis\Command;
/**
* Base class for Redis commands.
- *
- * @author Daniele Alessandri
*/
abstract class Command implements CommandInterface
{
private $slot;
- private $arguments = array();
-
- /**
- * Returns a filtered array of the arguments.
- *
- * @param array $arguments List of arguments.
- *
- * @return array
- */
- protected function filterArguments(array $arguments)
- {
- return $arguments;
- }
+ private $arguments = [];
/**
* {@inheritdoc}
*/
public function setArguments(array $arguments)
{
- $this->arguments = $this->filterArguments($arguments);
+ $this->arguments = $arguments;
unset($this->slot);
}
@@ -82,9 +69,7 @@ abstract class Command implements CommandInterface
*/
public function getSlot()
{
- if (isset($this->slot)) {
- return $this->slot;
- }
+ return $this->slot ?? null;
}
/**
@@ -104,7 +89,7 @@ abstract class Command implements CommandInterface
*/
public static function normalizeArguments(array $arguments)
{
- if (count($arguments) === 1 && is_array($arguments[0])) {
+ if (count($arguments) === 1 && isset($arguments[0]) && is_array($arguments[0])) {
return $arguments[0];
}
@@ -121,9 +106,21 @@ abstract class Command implements CommandInterface
public static function normalizeVariadic(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
- return array_merge(array($arguments[0]), $arguments[1]);
+ return array_merge([$arguments[0]], $arguments[1]);
}
return $arguments;
}
+
+ /**
+ * Remove all false values from arguments.
+ *
+ * @return void
+ */
+ public function filterArguments(): void
+ {
+ $this->arguments = array_filter($this->arguments, static function ($argument) {
+ return $argument !== false && $argument !== null;
+ });
+ }
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php b/plugins/cache-redis/Predis/Command/CommandInterface.php
similarity index 90%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php
rename to plugins/cache-redis/Predis/Command/CommandInterface.php
index 9f349e1df..20480316d 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php
+++ b/plugins/cache-redis/Predis/Command/CommandInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,8 +14,6 @@ namespace Predis\Command;
/**
* Defines an abstraction representing a Redis command.
- *
- * @author Daniele Alessandri
*/
interface CommandInterface
{
@@ -73,7 +72,7 @@ interface CommandInterface
/**
* Parses a raw response and returns a PHP object.
*
- * @param string $data Binary string containing the whole response.
+ * @param string|array|null $data Binary string containing the whole response.
*
* @return mixed
*/
diff --git a/plugins/cache-redis/Predis/Command/Factory.php b/plugins/cache-redis/Predis/Command/Factory.php
new file mode 100644
index 000000000..ed94dc572
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Factory.php
@@ -0,0 +1,143 @@
+getCommandClass($commandID) === null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the FQCN of a class that represents the specified command ID.
+ *
+ * @codeCoverageIgnore
+ *
+ * @param string $commandID Command ID
+ *
+ * @return string|null
+ */
+ public function getCommandClass(string $commandID): ?string
+ {
+ return $this->commands[strtoupper($commandID)] ?? null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(string $commandID, array $arguments = []): CommandInterface
+ {
+ if (!$commandClass = $this->getCommandClass($commandID)) {
+ $commandID = strtoupper($commandID);
+
+ throw new ClientException("Command `$commandID` is not a registered Redis command.");
+ }
+
+ $command = new $commandClass();
+ $command->setArguments($arguments);
+
+ if (isset($this->processor)) {
+ $this->processor->process($command);
+ }
+
+ return $command;
+ }
+
+ /**
+ * Defines a command in the factory.
+ *
+ * Only classes implementing Predis\Command\CommandInterface are allowed to
+ * handle a command. If the command specified by its ID is already handled
+ * by the factory, the underlying command class is replaced by the new one.
+ *
+ * @param string $commandID Command ID
+ * @param string $commandClass FQCN of a class implementing Predis\Command\CommandInterface
+ *
+ * @throws InvalidArgumentException
+ */
+ public function define(string $commandID, string $commandClass): void
+ {
+ if (!is_a($commandClass, 'Predis\Command\CommandInterface', true)) {
+ throw new InvalidArgumentException(
+ "Class $commandClass must implement Predis\Command\CommandInterface"
+ );
+ }
+
+ $this->commands[strtoupper($commandID)] = $commandClass;
+ }
+
+ /**
+ * Undefines a command in the factory.
+ *
+ * When the factory already has a class handler associated to the specified
+ * command ID it is removed from the map of known commands. Nothing happens
+ * when the command is not handled by the factory.
+ *
+ * @param string $commandID Command ID
+ */
+ public function undefine(string $commandID): void
+ {
+ unset($this->commands[strtoupper($commandID)]);
+ }
+
+ /**
+ * Sets a command processor for processing command arguments.
+ *
+ * Command processors are used to process and transform arguments of Redis
+ * commands before their newly created instances are returned to the caller
+ * of "create()".
+ *
+ * A NULL value can be used to effectively unset any processor if previously
+ * set for the command factory.
+ *
+ * @param ProcessorInterface|null $processor Command processor or NULL value.
+ */
+ public function setProcessor(?ProcessorInterface $processor): void
+ {
+ $this->processor = $processor;
+ }
+
+ /**
+ * Returns the current command processor.
+ *
+ * @return ProcessorInterface|null
+ */
+ public function getProcessor(): ?ProcessorInterface
+ {
+ return $this->processor;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/FactoryInterface.php b/plugins/cache-redis/Predis/Command/FactoryInterface.php
new file mode 100644
index 000000000..e81951086
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/FactoryInterface.php
@@ -0,0 +1,42 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -13,8 +14,6 @@ namespace Predis\Command;
/**
* Defines a command whose keys can be prefixed.
- *
- * @author Daniele Alessandri
*/
interface PrefixableCommandInterface extends CommandInterface
{
diff --git a/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php b/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php
new file mode 100644
index 000000000..d7a30c880
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php
@@ -0,0 +1,577 @@
+prefix = $prefix;
+
+ $prefixFirst = static::class . '::first';
+ $prefixAll = static::class . '::all';
+ $prefixInterleaved = static::class . '::interleaved';
+ $prefixSkipFirst = static::class . '::skipFirst';
+ $prefixSkipLast = static::class . '::skipLast';
+ $prefixSort = static::class . '::sort';
+ $prefixEvalKeys = static::class . '::evalKeys';
+ $prefixZsetStore = static::class . '::zsetStore';
+ $prefixMigrate = static::class . '::migrate';
+ $prefixGeoradius = static::class . '::georadius';
+
+ $this->commands = [
+ /* ---------------- Redis 1.2 ---------------- */
+ 'EXISTS' => $prefixAll,
+ 'DEL' => $prefixAll,
+ 'TYPE' => $prefixFirst,
+ 'KEYS' => $prefixFirst,
+ 'RENAME' => $prefixAll,
+ 'RENAMENX' => $prefixAll,
+ 'EXPIRE' => $prefixFirst,
+ 'EXPIREAT' => $prefixFirst,
+ 'TTL' => $prefixFirst,
+ 'MOVE' => $prefixFirst,
+ 'SORT' => $prefixSort,
+ 'DUMP' => $prefixFirst,
+ 'RESTORE' => $prefixFirst,
+ 'SET' => $prefixFirst,
+ 'SETNX' => $prefixFirst,
+ 'MSET' => $prefixInterleaved,
+ 'MSETNX' => $prefixInterleaved,
+ 'GET' => $prefixFirst,
+ 'MGET' => $prefixAll,
+ 'GETSET' => $prefixFirst,
+ 'INCR' => $prefixFirst,
+ 'INCRBY' => $prefixFirst,
+ 'DECR' => $prefixFirst,
+ 'DECRBY' => $prefixFirst,
+ 'RPUSH' => $prefixFirst,
+ 'LPUSH' => $prefixFirst,
+ 'LLEN' => $prefixFirst,
+ 'LRANGE' => $prefixFirst,
+ 'LTRIM' => $prefixFirst,
+ 'LINDEX' => $prefixFirst,
+ 'LSET' => $prefixFirst,
+ 'LREM' => $prefixFirst,
+ 'LPOP' => $prefixFirst,
+ 'RPOP' => $prefixFirst,
+ 'RPOPLPUSH' => $prefixAll,
+ 'SADD' => $prefixFirst,
+ 'SREM' => $prefixFirst,
+ 'SPOP' => $prefixFirst,
+ 'SMOVE' => $prefixSkipLast,
+ 'SCARD' => $prefixFirst,
+ 'SISMEMBER' => $prefixFirst,
+ 'SINTER' => $prefixAll,
+ 'SINTERSTORE' => $prefixAll,
+ 'SUNION' => $prefixAll,
+ 'SUNIONSTORE' => $prefixAll,
+ 'SDIFF' => $prefixAll,
+ 'SDIFFSTORE' => $prefixAll,
+ 'SMEMBERS' => $prefixFirst,
+ 'SRANDMEMBER' => $prefixFirst,
+ 'ZADD' => $prefixFirst,
+ 'ZINCRBY' => $prefixFirst,
+ 'ZREM' => $prefixFirst,
+ 'ZRANGE' => $prefixFirst,
+ 'ZREVRANGE' => $prefixFirst,
+ 'ZRANGEBYSCORE' => $prefixFirst,
+ 'ZCARD' => $prefixFirst,
+ 'ZSCORE' => $prefixFirst,
+ 'ZREMRANGEBYSCORE' => $prefixFirst,
+ /* ---------------- Redis 2.0 ---------------- */
+ 'SETEX' => $prefixFirst,
+ 'APPEND' => $prefixFirst,
+ 'SUBSTR' => $prefixFirst,
+ 'BLPOP' => $prefixSkipLast,
+ 'BRPOP' => $prefixSkipLast,
+ 'ZUNIONSTORE' => $prefixZsetStore,
+ 'ZINTERSTORE' => $prefixZsetStore,
+ 'ZCOUNT' => $prefixFirst,
+ 'ZRANK' => $prefixFirst,
+ 'ZREVRANK' => $prefixFirst,
+ 'ZREMRANGEBYRANK' => $prefixFirst,
+ 'HSET' => $prefixFirst,
+ 'HSETNX' => $prefixFirst,
+ 'HMSET' => $prefixFirst,
+ 'HINCRBY' => $prefixFirst,
+ 'HGET' => $prefixFirst,
+ 'HMGET' => $prefixFirst,
+ 'HDEL' => $prefixFirst,
+ 'HEXISTS' => $prefixFirst,
+ 'HLEN' => $prefixFirst,
+ 'HKEYS' => $prefixFirst,
+ 'HVALS' => $prefixFirst,
+ 'HGETALL' => $prefixFirst,
+ 'SUBSCRIBE' => $prefixAll,
+ 'UNSUBSCRIBE' => $prefixAll,
+ 'PSUBSCRIBE' => $prefixAll,
+ 'PUNSUBSCRIBE' => $prefixAll,
+ 'PUBLISH' => $prefixFirst,
+ /* ---------------- Redis 2.2 ---------------- */
+ 'PERSIST' => $prefixFirst,
+ 'STRLEN' => $prefixFirst,
+ 'SETRANGE' => $prefixFirst,
+ 'GETRANGE' => $prefixFirst,
+ 'SETBIT' => $prefixFirst,
+ 'GETBIT' => $prefixFirst,
+ 'RPUSHX' => $prefixFirst,
+ 'LPUSHX' => $prefixFirst,
+ 'LINSERT' => $prefixFirst,
+ 'BRPOPLPUSH' => $prefixSkipLast,
+ 'ZREVRANGEBYSCORE' => $prefixFirst,
+ 'WATCH' => $prefixAll,
+ /* ---------------- Redis 2.6 ---------------- */
+ 'PTTL' => $prefixFirst,
+ 'PEXPIRE' => $prefixFirst,
+ 'PEXPIREAT' => $prefixFirst,
+ 'PSETEX' => $prefixFirst,
+ 'INCRBYFLOAT' => $prefixFirst,
+ 'BITOP' => $prefixSkipFirst,
+ 'BITCOUNT' => $prefixFirst,
+ 'HINCRBYFLOAT' => $prefixFirst,
+ 'EVAL' => $prefixEvalKeys,
+ 'EVALSHA' => $prefixEvalKeys,
+ 'MIGRATE' => $prefixMigrate,
+ /* ---------------- Redis 2.8 ---------------- */
+ 'SSCAN' => $prefixFirst,
+ 'ZSCAN' => $prefixFirst,
+ 'HSCAN' => $prefixFirst,
+ 'PFADD' => $prefixFirst,
+ 'PFCOUNT' => $prefixAll,
+ 'PFMERGE' => $prefixAll,
+ 'ZLEXCOUNT' => $prefixFirst,
+ 'ZRANGEBYLEX' => $prefixFirst,
+ 'ZREMRANGEBYLEX' => $prefixFirst,
+ 'ZREVRANGEBYLEX' => $prefixFirst,
+ 'BITPOS' => $prefixFirst,
+ /* ---------------- Redis 3.2 ---------------- */
+ 'HSTRLEN' => $prefixFirst,
+ 'BITFIELD' => $prefixFirst,
+ 'GEOADD' => $prefixFirst,
+ 'GEOHASH' => $prefixFirst,
+ 'GEOPOS' => $prefixFirst,
+ 'GEODIST' => $prefixFirst,
+ 'GEORADIUS' => $prefixGeoradius,
+ 'GEORADIUSBYMEMBER' => $prefixGeoradius,
+ /* ---------------- Redis 5.0 ---------------- */
+ 'XADD' => $prefixFirst,
+ 'XRANGE' => $prefixFirst,
+ 'XREVRANGE' => $prefixFirst,
+ 'XDEL' => $prefixFirst,
+ 'XLEN' => $prefixFirst,
+ 'XACK' => $prefixFirst,
+ 'XTRIM' => $prefixFirst,
+
+ /* ---------------- Redis 6.2 ---------------- */
+ 'GETDEL' => $prefixFirst,
+
+ /* ---------------- Redis 7.0 ---------------- */
+ 'EXPIRETIME' => $prefixFirst,
+
+ /* RedisJSON */
+ 'JSON.ARRAPPEND' => $prefixFirst,
+ 'JSON.ARRINDEX' => $prefixFirst,
+ 'JSON.ARRINSERT' => $prefixFirst,
+ 'JSON.ARRLEN' => $prefixFirst,
+ 'JSON.ARRPOP' => $prefixFirst,
+ 'JSON.ARRTRIM' => $prefixFirst,
+ 'JSON.CLEAR' => $prefixFirst,
+ 'JSON.DEBUG MEMORY' => $prefixFirst,
+ 'JSON.DEL' => $prefixFirst,
+ 'JSON.FORGET' => $prefixFirst,
+ 'JSON.GET' => $prefixFirst,
+ 'JSON.MGET' => $prefixAll,
+ 'JSON.NUMINCRBY' => $prefixFirst,
+ 'JSON.OBJKEYS' => $prefixFirst,
+ 'JSON.OBJLEN' => $prefixFirst,
+ 'JSON.RESP' => $prefixFirst,
+ 'JSON.SET' => $prefixFirst,
+ 'JSON.STRAPPEND' => $prefixFirst,
+ 'JSON.STRLEN' => $prefixFirst,
+ 'JSON.TOGGLE' => $prefixFirst,
+ 'JSON.TYPE' => $prefixFirst,
+
+ /* RedisBloom */
+ 'BF.ADD' => $prefixFirst,
+ 'BF.EXISTS' => $prefixFirst,
+ 'BF.INFO' => $prefixFirst,
+ 'BF.INSERT' => $prefixFirst,
+ 'BF.LOADCHUNK' => $prefixFirst,
+ 'BF.MADD' => $prefixFirst,
+ 'BF.MEXISTS' => $prefixFirst,
+ 'BF.RESERVE' => $prefixFirst,
+ 'BF.SCANDUMP' => $prefixFirst,
+ 'CF.ADD' => $prefixFirst,
+ 'CF.ADDNX' => $prefixFirst,
+ 'CF.COUNT' => $prefixFirst,
+ 'CF.DEL' => $prefixFirst,
+ 'CF.EXISTS' => $prefixFirst,
+ 'CF.INFO' => $prefixFirst,
+ 'CF.INSERT' => $prefixFirst,
+ 'CF.INSERTNX' => $prefixFirst,
+ 'CF.LOADCHUNK' => $prefixFirst,
+ 'CF.MEXISTS' => $prefixFirst,
+ 'CF.RESERVE' => $prefixFirst,
+ 'CF.SCANDUMP' => $prefixFirst,
+ 'CMS.INCRBY' => $prefixFirst,
+ 'CMS.INFO' => $prefixFirst,
+ 'CMS.INITBYDIM' => $prefixFirst,
+ 'CMS.INITBYPROB' => $prefixFirst,
+ 'CMS.QUERY' => $prefixFirst,
+ 'TDIGEST.ADD' => $prefixFirst,
+ 'TDIGEST.BYRANK' => $prefixFirst,
+ 'TDIGEST.BYREVRANK' => $prefixFirst,
+ 'TDIGEST.CDF' => $prefixFirst,
+ 'TDIGEST.CREATE' => $prefixFirst,
+ 'TDIGEST.INFO' => $prefixFirst,
+ 'TDIGEST.MAX' => $prefixFirst,
+ 'TDIGEST.MIN' => $prefixFirst,
+ 'TDIGEST.QUANTILE' => $prefixFirst,
+ 'TDIGEST.RANK' => $prefixFirst,
+ 'TDIGEST.RESET' => $prefixFirst,
+ 'TDIGEST.REVRANK' => $prefixFirst,
+ 'TDIGEST.TRIMMED_MEAN' => $prefixFirst,
+ 'TOPK.ADD' => $prefixFirst,
+ 'TOPK.INCRBY' => $prefixFirst,
+ 'TOPK.INFO' => $prefixFirst,
+ 'TOPK.LIST' => $prefixFirst,
+ 'TOPK.QUERY' => $prefixFirst,
+ 'TOPK.RESERVE' => $prefixFirst,
+
+ /* RediSearch */
+ 'FT.AGGREGATE' => $prefixFirst,
+ 'FT.ALTER' => $prefixFirst,
+ 'FT.CREATE' => $prefixFirst,
+ 'FT.CURSOR DEL' => $prefixFirst,
+ 'FT.CURSOR READ' => $prefixFirst,
+ 'FT.DROPINDEX' => $prefixFirst,
+ 'FT.EXPLAIN' => $prefixFirst,
+ 'FT.INFO' => $prefixFirst,
+ 'FT.PROFILE' => $prefixFirst,
+ 'FT.SEARCH' => $prefixFirst,
+ 'FT.SPELLCHECK' => $prefixFirst,
+ 'FT.SYNDUMP' => $prefixFirst,
+ 'FT.SYNUPDATE' => $prefixFirst,
+ 'FT.TAGVALS' => $prefixFirst,
+
+ /* Redis TimeSeries */
+ 'TS.ADD' => $prefixFirst,
+ 'TS.ALTER' => $prefixFirst,
+ 'TS.CREATE' => $prefixFirst,
+ 'TS.DECRBY' => $prefixFirst,
+ 'TS.DEL' => $prefixFirst,
+ 'TS.GET' => $prefixFirst,
+ 'TS.INCRBY' => $prefixFirst,
+ 'TS.INFO' => $prefixFirst,
+ 'TS.MGET' => $prefixFirst,
+ 'TS.MRANGE' => $prefixFirst,
+ 'TS.MREVRANGE' => $prefixFirst,
+ 'TS.QUERYINDEX' => $prefixFirst,
+ 'TS.RANGE' => $prefixFirst,
+ 'TS.REVRANGE' => $prefixFirst,
+ ];
+ }
+
+ /**
+ * Sets a prefix that is applied to all the keys.
+ *
+ * @param string $prefix Prefix for the keys.
+ */
+ public function setPrefix($prefix)
+ {
+ $this->prefix = $prefix;
+ }
+
+ /**
+ * Gets the current prefix.
+ *
+ * @return string
+ */
+ public function getPrefix()
+ {
+ return $this->prefix;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function process(CommandInterface $command)
+ {
+ if ($command instanceof PrefixableCommandInterface) {
+ $command->prefixKeys($this->prefix);
+ } elseif (isset($this->commands[$commandID = strtoupper($command->getId())])) {
+ $this->commands[$commandID]($command, $this->prefix);
+ }
+ }
+
+ /**
+ * Sets an handler for the specified command ID.
+ *
+ * The callback signature must have 2 parameters of the following types:
+ *
+ * - Predis\Command\CommandInterface (command instance)
+ * - String (prefix)
+ *
+ * When the callback argument is omitted or NULL, the previously
+ * associated handler for the specified command ID is removed.
+ *
+ * @param string $commandID The ID of the command to be handled.
+ * @param mixed $callback A valid callable object or NULL.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function setCommandHandler($commandID, $callback = null)
+ {
+ $commandID = strtoupper($commandID);
+
+ if (!isset($callback)) {
+ unset($this->commands[$commandID]);
+
+ return;
+ }
+
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException(
+ 'Callback must be a valid callable object or NULL'
+ );
+ }
+
+ $this->commands[$commandID] = $callback;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->getPrefix();
+ }
+
+ /**
+ * Applies the specified prefix only the first argument.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function first(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $arguments[0] = "$prefix{$arguments[0]}";
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to all the arguments.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function all(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ foreach ($arguments as &$key) {
+ $key = "$prefix$key";
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix only to even arguments in the list.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function interleaved(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $length = count($arguments);
+
+ for ($i = 0; $i < $length; $i += 2) {
+ $arguments[$i] = "$prefix{$arguments[$i]}";
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to all the arguments but the first one.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function skipFirst(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $length = count($arguments);
+
+ for ($i = 1; $i < $length; ++$i) {
+ $arguments[$i] = "$prefix{$arguments[$i]}";
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to all the arguments but the last one.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function skipLast(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $length = count($arguments);
+
+ for ($i = 0; $i < $length - 1; ++$i) {
+ $arguments[$i] = "$prefix{$arguments[$i]}";
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to the keys of a SORT command.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function sort(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $arguments[0] = "$prefix{$arguments[0]}";
+
+ if (($count = count($arguments)) > 1) {
+ for ($i = 1; $i < $count; ++$i) {
+ switch (strtoupper($arguments[$i])) {
+ case 'BY':
+ case 'STORE':
+ $arguments[$i] = "$prefix{$arguments[++$i]}";
+ break;
+
+ case 'GET':
+ $value = $arguments[++$i];
+ if ($value !== '#') {
+ $arguments[$i] = "$prefix$value";
+ }
+ break;
+
+ case 'LIMIT':
+ $i += 2;
+ break;
+ }
+ }
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to the keys of an EVAL-based command.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function evalKeys(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ for ($i = 2; $i < $arguments[1] + 2; ++$i) {
+ $arguments[$i] = "$prefix{$arguments[$i]}";
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to the keys of Z[INTERSECTION|UNION]STORE.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function zsetStore(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $arguments[0] = "$prefix{$arguments[0]}";
+ $length = ((int) $arguments[1]) + 2;
+
+ for ($i = 2; $i < $length; ++$i) {
+ $arguments[$i] = "$prefix{$arguments[$i]}";
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to the key of a MIGRATE command.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function migrate(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $arguments[2] = "$prefix{$arguments[2]}";
+ $command->setRawArguments($arguments);
+ }
+ }
+
+ /**
+ * Applies the specified prefix to the key of a GEORADIUS command.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $prefix Prefix string.
+ */
+ public static function georadius(CommandInterface $command, $prefix)
+ {
+ if ($arguments = $command->getArguments()) {
+ $arguments[0] = "$prefix{$arguments[0]}";
+ $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
+
+ if (($count = count($arguments)) > $startIndex) {
+ for ($i = $startIndex; $i < $count; ++$i) {
+ switch (strtoupper($arguments[$i])) {
+ case 'STORE':
+ case 'STOREDIST':
+ $arguments[$i] = "$prefix{$arguments[++$i]}";
+ break;
+ }
+ }
+ }
+
+ $command->setRawArguments($arguments);
+ }
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorChain.php b/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php
similarity index 69%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorChain.php
rename to plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php
index 0a4768b0a..1ce915e2b 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorChain.php
+++ b/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,21 +12,24 @@
namespace Predis\Command\Processor;
+use ArrayAccess;
+use ArrayIterator;
+use InvalidArgumentException;
use Predis\Command\CommandInterface;
+use ReturnTypeWillChange;
+use Traversable;
/**
* Default implementation of a command processors chain.
- *
- * @author Daniele Alessandri
*/
-class ProcessorChain implements \ArrayAccess, ProcessorInterface
+class ProcessorChain implements ArrayAccess, ProcessorInterface
{
- private $processors = array();
+ private $processors = [];
/**
* @param array $processors List of instances of ProcessorInterface.
*/
- public function __construct($processors = array())
+ public function __construct($processors = [])
{
foreach ($processors as $processor) {
$this->add($processor);
@@ -71,11 +75,11 @@ class ProcessorChain implements \ArrayAccess, ProcessorInterface
/**
* Returns an iterator over the list of command processor in the chain.
*
- * @return \ArrayIterator
+ * @return Traversable
*/
public function getIterator()
{
- return new \ArrayIterator($this->processors);
+ return new ArrayIterator($this->processors);
}
/**
@@ -89,30 +93,36 @@ class ProcessorChain implements \ArrayAccess, ProcessorInterface
}
/**
- * {@inheritdoc}
+ * @param int $index
+ * @return bool
*/
+ #[ReturnTypeWillChange]
public function offsetExists($index)
{
return isset($this->processors[$index]);
}
/**
- * {@inheritdoc}
+ * @param int $index
+ * @return ProcessorInterface
*/
+ #[ReturnTypeWillChange]
public function offsetGet($index)
{
return $this->processors[$index];
}
/**
- * {@inheritdoc}
+ * @param int $index
+ * @param ProcessorInterface $processor
+ * @return void
*/
+ #[ReturnTypeWillChange]
public function offsetSet($index, $processor)
{
if (!$processor instanceof ProcessorInterface) {
- throw new \InvalidArgumentException(
- 'A processor chain accepts only instances of '.
- "'Predis\Command\Processor\ProcessorInterface'."
+ throw new InvalidArgumentException(
+ 'Processor chain accepts only instances of `Predis\Command\Processor\ProcessorInterface`'
);
}
@@ -120,8 +130,10 @@ class ProcessorChain implements \ArrayAccess, ProcessorInterface
}
/**
- * {@inheritdoc}
+ * @param int $index
+ * @return void
*/
+ #[ReturnTypeWillChange]
public function offsetUnset($index)
{
unset($this->processors[$index]);
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php b/plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php
similarity index 84%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php
rename to plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php
index 2f9105802..f915b9eb6 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php
+++ b/plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -15,8 +16,6 @@ use Predis\Command\CommandInterface;
/**
* A command processor processes Redis commands before they are sent to Redis.
- *
- * @author Daniele Alessandri
*/
interface ProcessorInterface
{
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/RawCommand.php b/plugins/cache-redis/Predis/Command/RawCommand.php
similarity index 55%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/RawCommand.php
rename to plugins/cache-redis/Predis/Command/RawCommand.php
index 2dd48ca17..61b223112 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/RawCommand.php
+++ b/plugins/cache-redis/Predis/Command/RawCommand.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -12,52 +13,45 @@
namespace Predis\Command;
/**
- * Class for generic "anonymous" Redis commands.
+ * Class representing a generic Redis command.
*
- * This command class does not filter input arguments or parse responses, but
- * can be used to leverage the standard Predis API to execute any command simply
- * by providing the needed arguments following the command signature as defined
- * by Redis in its documentation.
+ * Arguments and responses for these commands are not normalized and they follow
+ * what is defined by the Redis documentation.
*
- * @author Daniele Alessandri
+ * Raw commands can be useful when implementing higher level abstractions on top
+ * of Predis\Client or managing internals like Redis Sentinel or Cluster as they
+ * are not potentially subject to hijacking from third party libraries when they
+ * override command handlers for standard Redis commands.
*/
-class RawCommand implements CommandInterface
+final class RawCommand implements CommandInterface
{
private $slot;
private $commandID;
private $arguments;
/**
- * @param array $arguments Command ID and its arguments.
- *
- * @throws \InvalidArgumentException
+ * @param string $commandID Command ID
+ * @param array $arguments Command arguments
*/
- public function __construct(array $arguments)
+ public function __construct($commandID, array $arguments = [])
{
- if (!$arguments) {
- throw new \InvalidArgumentException(
- 'The arguments array must contain at least the command ID.'
- );
- }
-
- $this->commandID = strtoupper(array_shift($arguments));
- $this->arguments = $arguments;
+ $this->commandID = strtoupper($commandID);
+ $this->setArguments($arguments);
}
/**
* Creates a new raw command using a variadic method.
*
- * @param string $commandID Redis command ID.
- * @param string ... Arguments list for the command.
+ * @param string $commandID Redis command ID
+ * @param string ...$args Arguments list for the command
*
* @return CommandInterface
*/
- public static function create($commandID /* [ $arg, ... */)
+ public static function create($commandID, ...$args)
{
$arguments = func_get_args();
- $command = new self($arguments);
- return $command;
+ return new static(array_shift($arguments), $arguments);
}
/**
@@ -116,9 +110,7 @@ class RawCommand implements CommandInterface
*/
public function getSlot()
{
- if (isset($this->slot)) {
- return $this->slot;
- }
+ return $this->slot ?? null;
}
/**
diff --git a/plugins/cache-redis/Predis/Command/RawFactory.php b/plugins/cache-redis/Predis/Command/RawFactory.php
new file mode 100644
index 000000000..2c0637f22
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/RawFactory.php
@@ -0,0 +1,43 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/append
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/append
*/
-class StringAppend extends Command
+class APPEND extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionAuth.php b/plugins/cache-redis/Predis/Command/Redis/AUTH.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionAuth.php
rename to plugins/cache-redis/Predis/Command/Redis/AUTH.php
index c8c9dedce..cc2020e5f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionAuth.php
+++ b/plugins/cache-redis/Predis/Command/Redis/AUTH.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/auth
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/auth
*/
-class ConnectionAuth extends Command
+class AUTH extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php b/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php
new file mode 100644
index 000000000..ed4a53b80
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php
@@ -0,0 +1,43 @@
+setKeys($arguments, false);
+ }
+
+ public function parseResponse($data)
+ {
+ $key = array_shift($data);
+
+ if (null === $key) {
+ return [$key];
+ }
+
+ return array_combine([$key], [[$data[0] => $data[1]]]);
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundRewriteAOF.php b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php
similarity index 67%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundRewriteAOF.php
rename to plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php
index c66a294e5..97d443d90 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundRewriteAOF.php
+++ b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/bgrewriteaof
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/bgrewriteaof
*/
-class ServerBackgroundRewriteAOF extends Command
+class BGREWRITEAOF extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundSave.php b/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php
similarity index 68%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundSave.php
rename to plugins/cache-redis/Predis/Command/Redis/BGSAVE.php
index 4bf67ef30..9be85773f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundSave.php
+++ b/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/bgsave
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/bgsave
*/
-class ServerBackgroundSave extends Command
+class BGSAVE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php b/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php
new file mode 100644
index 000000000..859daf4a6
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php
@@ -0,0 +1,34 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/bitop
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/bitop
*/
-class StringBitOp extends Command
+class BITOP extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,14 +30,14 @@ class StringBitOp extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 3 && is_array($arguments[2])) {
- list($operation, $destination) = $arguments;
+ [$operation, $destination] = $arguments;
$arguments = $arguments[2];
array_unshift($arguments, $operation, $destination);
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BITPOS.php b/plugins/cache-redis/Predis/Command/Redis/BITPOS.php
new file mode 100644
index 000000000..6ea418817
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BITPOS.php
@@ -0,0 +1,34 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/blpop
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/blpop
*/
-class ListPopFirstBlocking extends Command
+class BLPOP extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,13 +30,13 @@ class ListPopFirstBlocking extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[0])) {
- list($arguments, $timeout) = $arguments;
+ [$arguments, $timeout] = $arguments;
array_push($arguments, $timeout);
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BRPOP.php b/plugins/cache-redis/Predis/Command/Redis/BRPOP.php
new file mode 100644
index 000000000..cf88dc906
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BRPOP.php
@@ -0,0 +1,42 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/brpoplpush
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/brpoplpush
*/
-class ListPopLastPushHeadBlocking extends Command
+class BRPOPLPUSH extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php b/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php
new file mode 100644
index 000000000..2ed6f7893
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php
@@ -0,0 +1,30 @@
+ 'CAPACITY',
+ 'size' => 'SIZE',
+ 'filters' => 'FILTERS',
+ 'items' => 'ITEMS',
+ 'expansion' => 'EXPANSION',
+ ];
+
+ public function getId()
+ {
+ return 'BF.INFO';
+ }
+
+ public function setArguments(array $arguments)
+ {
+ if (isset($arguments[1])) {
+ $modifier = array_pop($arguments);
+
+ if ($modifier === '') {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (!in_array(strtoupper($modifier), $this->modifierEnum)) {
+ $enumValues = implode(', ', array_keys($this->modifierEnum));
+ throw new UnexpectedValueException("Argument accepts only: {$enumValues} values");
+ }
+
+ $arguments[] = $this->modifierEnum[strtolower($modifier)];
+ }
+
+ parent::setArguments($arguments);
+ }
+
+ public function parseResponse($data)
+ {
+ if (count($data) > 1) {
+ $result = [];
+
+ for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) {
+ if (array_key_exists($i + 1, $data)) {
+ $result[(string) $data[$i]] = $data[++$i];
+ }
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php
new file mode 100644
index 000000000..50e23eeaf
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php
@@ -0,0 +1,72 @@
+setNoCreate($arguments);
+ $arguments = $this->getArguments();
+
+ if (array_key_exists(5, $arguments) && $arguments[5]) {
+ $arguments[5] = 'NONSCALING';
+ }
+
+ $this->setItems($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setExpansion($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setErrorRate($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setCapacity($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php
new file mode 100644
index 000000000..76a78e997
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php
@@ -0,0 +1,28 @@
+setExpansion($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php
new file mode 100644
index 000000000..f8576d2d0
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/client-list
- * @link http://redis.io/commands/client-kill
- * @link http://redis.io/commands/client-getname
- * @link http://redis.io/commands/client-setname
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/client-list
+ * @see http://redis.io/commands/client-kill
+ * @see http://redis.io/commands/client-getname
+ * @see http://redis.io/commands/client-setname
*/
-class ServerClient extends Command
+class CLIENT extends RedisCommand
{
/**
* {@inheritdoc}
@@ -44,7 +45,7 @@ class ServerClient extends Command
case 'SETNAME':
default:
return $data;
- }
+ } // @codeCoverageIgnore
}
/**
@@ -56,13 +57,13 @@ class ServerClient extends Command
*/
protected function parseClientList($data)
{
- $clients = array();
+ $clients = [];
foreach (explode("\n", $data, -1) as $clientData) {
- $client = array();
+ $client = [];
foreach (explode(' ', $clientData) as $kv) {
- @list($k, $v) = explode('=', $kv);
+ @[$k, $v] = explode('=', $kv);
$client[$k] = $v;
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php
new file mode 100644
index 000000000..3bbb77cc8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php
@@ -0,0 +1,26 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/config-set
- * @link http://redis.io/commands/config-get
- * @link http://redis.io/commands/config-resetstat
- * @link http://redis.io/commands/config-rewrite
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/config-set
+ * @see http://redis.io/commands/config-get
+ * @see http://redis.io/commands/config-resetstat
+ * @see http://redis.io/commands/config-rewrite
*/
-class ServerConfig extends Command
+class CONFIG extends RedisCommand
{
/**
* {@inheritdoc}
@@ -35,7 +36,11 @@ class ServerConfig extends Command
public function parseResponse($data)
{
if (is_array($data)) {
- $result = array();
+ if ($data !== array_values($data)) {
+ return $data; // Relay
+ }
+
+ $result = [];
for ($i = 0; $i < count($data); ++$i) {
$result[$data[$i]] = $data[++$i];
diff --git a/plugins/cache-redis/Predis/Command/Redis/COPY.php b/plugins/cache-redis/Predis/Command/Redis/COPY.php
new file mode 100644
index 000000000..cb6ec659b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/COPY.php
@@ -0,0 +1,47 @@
+setDB($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setReplace($arguments);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php
new file mode 100644
index 000000000..2699d37e8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php
@@ -0,0 +1,28 @@
+client = $client;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function __call(string $subcommandID, array $arguments)
+ {
+ array_unshift($arguments, strtoupper($subcommandID));
+
+ return $this->client->executeCommand(
+ $this->client->createCommand($this->getContainerCommandId(), $arguments)
+ );
+ }
+
+ abstract public function getContainerCommandId(): string;
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php
new file mode 100644
index 000000000..b2925b19b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php
@@ -0,0 +1,29 @@
+ FunctionContainer::class,
+ ];
+
+ /**
+ * Creates container command.
+ *
+ * @param ClientInterface $client
+ * @param string $containerCommandID
+ * @return ContainerInterface
+ */
+ public static function create(ClientInterface $client, string $containerCommandID): ContainerInterface
+ {
+ $containerCommandID = strtoupper($containerCommandID);
+ $commandModule = self::resolveCommandModuleByPrefix($containerCommandID);
+
+ if (null !== $commandModule) {
+ if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $commandModule . '\\' . $containerCommandID)) {
+ return new $containerClass($client);
+ }
+
+ throw new UnexpectedValueException('Given module container command is not supported.');
+ }
+
+ if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $containerCommandID)) {
+ return new $containerClass($client);
+ }
+
+ if (array_key_exists($containerCommandID, self::$specialMappings)) {
+ $containerClass = self::$specialMappings[$containerCommandID];
+
+ return new $containerClass($client);
+ }
+
+ throw new UnexpectedValueException('Given container command is not supported.');
+ }
+
+ /**
+ * @param string $commandID
+ * @return string|null
+ */
+ private static function resolveCommandModuleByPrefix(string $commandID): ?string
+ {
+ $modules = ClientConfiguration::getModules();
+
+ foreach ($modules as $module) {
+ if (preg_match("/^{$module['commandPrefix']}/", $commandID)) {
+ return $module['name'];
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php
new file mode 100644
index 000000000..e30c539e3
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php
@@ -0,0 +1,33 @@
+ 1) {
+ $result = [];
+
+ for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) {
+ if (array_key_exists($i + 1, $data)) {
+ $result[(string) $data[$i]] = $data[++$i];
+ }
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php
new file mode 100644
index 000000000..8f1f23ff3
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php
@@ -0,0 +1,28 @@
+ 1) {
+ $result = [];
+
+ for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) {
+ if (array_key_exists($i + 1, $data)) {
+ $result[(string) $data[$i]] = $data[++$i];
+ }
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php
new file mode 100644
index 000000000..3a49a38d7
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php
@@ -0,0 +1,52 @@
+setNoCreate($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setItems($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setCapacity($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php
new file mode 100644
index 000000000..629327b4f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php
@@ -0,0 +1,27 @@
+setExpansion($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setMaxIterations($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setBucketSize($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php
new file mode 100644
index 000000000..59caa651c
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/dbsize
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/dbsize
*/
-class ServerDatabaseSize extends Command
+class DBSIZE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrement.php b/plugins/cache-redis/Predis/Command/Redis/DECR.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrement.php
rename to plugins/cache-redis/Predis/Command/Redis/DECR.php
index aa5808cd0..453f683a8 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrement.php
+++ b/plugins/cache-redis/Predis/Command/Redis/DECR.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/decr
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/decr
*/
-class StringDecrement extends Command
+class DECR extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrementBy.php b/plugins/cache-redis/Predis/Command/Redis/DECRBY.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrementBy.php
rename to plugins/cache-redis/Predis/Command/Redis/DECRBY.php
index cbf3e1124..15eace6be 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrementBy.php
+++ b/plugins/cache-redis/Predis/Command/Redis/DECRBY.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/decrby
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/decrby
*/
-class StringDecrementBy extends Command
+class DECRBY extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/DEL.php b/plugins/cache-redis/Predis/Command/Redis/DEL.php
new file mode 100644
index 000000000..359bbf4ff
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/DEL.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/dump
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/dump
*/
-class KeyDump extends Command
+class DUMP extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionEcho.php b/plugins/cache-redis/Predis/Command/Redis/ECHO_.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionEcho.php
rename to plugins/cache-redis/Predis/Command/Redis/ECHO_.php
index fd4960971..c04adea33 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionEcho.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ECHO_.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/echo
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/echo
*/
-class ConnectionEcho extends Command
+class ECHO_ extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerEvalSHA.php b/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php
similarity index 70%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerEvalSHA.php
rename to plugins/cache-redis/Predis/Command/Redis/EVALSHA.php
index 520a8e985..ee7a38087 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerEvalSHA.php
+++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php
@@ -3,20 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/evalsha
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/evalsha
*/
-class ServerEvalSHA extends ServerEval
+class EVALSHA extends EVAL_
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php
new file mode 100644
index 000000000..809a08757
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php
@@ -0,0 +1,27 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/eval
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/eval
*/
-class ServerEval extends Command
+class EVAL_ extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php
new file mode 100644
index 000000000..cef8bd35e
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php
@@ -0,0 +1,34 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/exec
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/exec
*/
-class TransactionExec extends Command
+class EXEC extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/EXISTS.php b/plugins/cache-redis/Predis/Command/Redis/EXISTS.php
new file mode 100644
index 000000000..8731c9d84
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/EXISTS.php
@@ -0,0 +1,29 @@
+setTimeout($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setTo($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/FCALL.php b/plugins/cache-redis/Predis/Command/Redis/FCALL.php
new file mode 100644
index 000000000..cc5639321
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/FCALL.php
@@ -0,0 +1,33 @@
+ 2) {
+ for ($i = 2, $iMax = count($arguments); $i < $iMax; $i++) {
+ $processedArguments[] = $arguments[$i];
+ }
+ }
+
+ parent::setArguments($processedArguments);
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushAll.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushAll.php
rename to plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php
index c35b2ad6a..a03a3133d 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushAll.php
+++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/flushall
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/flushall
*/
-class ServerFlushAll extends Command
+class FLUSHALL extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushDatabase.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushDatabase.php
rename to plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php
index 3da6b320d..67a2d485b 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushDatabase.php
+++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/flushdb
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/flushdb
*/
-class ServerFlushDatabase extends Command
+class FLUSHDB extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php b/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php
new file mode 100644
index 000000000..7f4fde79a
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php
@@ -0,0 +1,50 @@
+strategyResolver = new SubcommandStrategyResolver();
+ }
+
+ public function getId()
+ {
+ return 'FUNCTION';
+ }
+
+ public function setArguments(array $arguments)
+ {
+ $strategy = $this->strategyResolver->resolve('functions', strtolower($arguments[0]));
+ $arguments = $strategy->processArguments($arguments);
+
+ parent::setArguments($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOADD.php b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php
new file mode 100644
index 000000000..56156b72b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php
@@ -0,0 +1,43 @@
+setSorting($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setWithCoord($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setWithDist($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setWithHash($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setCount($arguments, $arguments[5] ?? false);
+ $arguments = $this->getArguments();
+
+ $this->setFrom($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setBy($arguments);
+ $this->filterArguments();
+ }
+
+ public function parseResponse($data)
+ {
+ $parsedData = [];
+ $itemKey = '';
+
+ foreach ($data as $item) {
+ if (!is_array($item)) {
+ $parsedData[] = $item;
+ continue;
+ }
+
+ foreach ($item as $key => $itemRow) {
+ if ($key === 0) {
+ $itemKey = $itemRow;
+ continue;
+ }
+
+ if (is_string($itemRow)) {
+ $parsedData[$itemKey]['dist'] = round((float) $itemRow, 5);
+ } elseif (is_int($itemRow)) {
+ $parsedData[$itemKey]['hash'] = $itemRow;
+ } else {
+ $parsedData[$itemKey]['lng'] = round($itemRow[0], 5);
+ $parsedData[$itemKey]['lat'] = round($itemRow[1], 5);
+ }
+ }
+ }
+
+ return $parsedData;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php
new file mode 100644
index 000000000..6798db7ed
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php
@@ -0,0 +1,71 @@
+setStoreDist($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setCount($arguments, $arguments[6] ?? false);
+ $arguments = $this->getArguments();
+
+ $this->setSorting($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setFrom($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setBy($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGet.php b/plugins/cache-redis/Predis/Command/Redis/GET.php
similarity index 59%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGet.php
rename to plugins/cache-redis/Predis/Command/Redis/GET.php
index 138e915c2..e9177ab28 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGet.php
+++ b/plugins/cache-redis/Predis/Command/Redis/GET.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/get
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/get
*/
-class StringGet extends Command
+class GET extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetBit.php b/plugins/cache-redis/Predis/Command/Redis/GETBIT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetBit.php
rename to plugins/cache-redis/Predis/Command/Redis/GETBIT.php
index 3c5b4f9b7..00ee97085 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetBit.php
+++ b/plugins/cache-redis/Predis/Command/Redis/GETBIT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/getbit
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/getbit
*/
-class StringGetBit extends Command
+class GETBIT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/GETDEL.php b/plugins/cache-redis/Predis/Command/Redis/GETDEL.php
new file mode 100644
index 000000000..7e4df93c6
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/GETDEL.php
@@ -0,0 +1,23 @@
+ 'EX',
+ 'px' => 'PX',
+ 'exat' => 'EXAT',
+ 'pxat' => 'PXAT',
+ 'persist' => 'PERSIST',
+ ];
+
+ public function getId()
+ {
+ return 'GETEX';
+ }
+
+ public function setArguments(array $arguments)
+ {
+ if (!array_key_exists(1, $arguments) || $arguments[1] === '') {
+ parent::setArguments([$arguments[0]]);
+
+ return;
+ }
+
+ if (!in_array(strtoupper($arguments[1]), self::$modifierEnum)) {
+ $enumValues = implode(', ', array_keys(self::$modifierEnum));
+ throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
+ }
+
+ if ($arguments[1] === 'persist') {
+ parent::setArguments([$arguments[0], self::$modifierEnum[$arguments[1]]]);
+
+ return;
+ }
+
+ $arguments[1] = self::$modifierEnum[$arguments[1]];
+
+ if (!array_key_exists(2, $arguments)) {
+ throw new UnexpectedValueException('You should provide value for current modifier');
+ }
+
+ parent::setArguments($arguments);
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetRange.php b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetRange.php
rename to plugins/cache-redis/Predis/Command/Redis/GETRANGE.php
index bb10565b5..c6feb408b 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetRange.php
+++ b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/getrange
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/getrange
*/
-class StringGetRange extends Command
+class GETRANGE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetSet.php b/plugins/cache-redis/Predis/Command/Redis/GETSET.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetSet.php
rename to plugins/cache-redis/Predis/Command/Redis/GETSET.php
index b68870d4c..37a38619c 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetSet.php
+++ b/plugins/cache-redis/Predis/Command/Redis/GETSET.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/getset
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/getset
*/
-class StringGetSet extends Command
+class GETSET extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/HDEL.php b/plugins/cache-redis/Predis/Command/Redis/HDEL.php
new file mode 100644
index 000000000..bb1fba345
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/HDEL.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hget
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hget
*/
-class HashGet extends Command
+class HGET extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashGetAll.php b/plugins/cache-redis/Predis/Command/Redis/HGETALL.php
similarity index 61%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashGetAll.php
rename to plugins/cache-redis/Predis/Command/Redis/HGETALL.php
index d69867521..c5f566bae 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashGetAll.php
+++ b/plugins/cache-redis/Predis/Command/Redis/HGETALL.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hgetall
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hgetall
*/
-class HashGetAll extends Command
+class HGETALL extends RedisCommand
{
/**
* {@inheritdoc}
@@ -31,7 +32,11 @@ class HashGetAll extends Command
*/
public function parseResponse($data)
{
- $result = array();
+ if ($data !== array_values($data)) {
+ return $data; // Relay
+ }
+
+ $result = [];
for ($i = 0; $i < count($data); ++$i) {
$result[$data[$i]] = $data[++$i];
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementBy.php b/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementBy.php
rename to plugins/cache-redis/Predis/Command/Redis/HINCRBY.php
index a37359ffb..cf60ecea1 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementBy.php
+++ b/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hincrby
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hincrby
*/
-class HashIncrementBy extends Command
+class HINCRBY extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementByFloat.php b/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementByFloat.php
rename to plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php
index bce9714fc..566ee874a 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementByFloat.php
+++ b/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hincrbyfloat
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hincrbyfloat
*/
-class HashIncrementByFloat extends Command
+class HINCRBYFLOAT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashKeys.php b/plugins/cache-redis/Predis/Command/Redis/HKEYS.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashKeys.php
rename to plugins/cache-redis/Predis/Command/Redis/HKEYS.php
index 28266020a..43986ec3f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashKeys.php
+++ b/plugins/cache-redis/Predis/Command/Redis/HKEYS.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hkeys
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hkeys
*/
-class HashKeys extends Command
+class HKEYS extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashLength.php b/plugins/cache-redis/Predis/Command/Redis/HLEN.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashLength.php
rename to plugins/cache-redis/Predis/Command/Redis/HLEN.php
index d70926f1c..32903ea62 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashLength.php
+++ b/plugins/cache-redis/Predis/Command/Redis/HLEN.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hlen
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hlen
*/
-class HashLength extends Command
+class HLEN extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/HMGET.php b/plugins/cache-redis/Predis/Command/Redis/HMGET.php
new file mode 100644
index 000000000..077373f68
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/HMGET.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hmset
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hmset
*/
-class HashSetMultiple extends Command
+class HMSET extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,10 +30,10 @@ class HashSetMultiple extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
- $flattenedKVs = array($arguments[0]);
+ $flattenedKVs = [$arguments[0]];
$args = $arguments[1];
foreach ($args as $k => $v) {
@@ -40,9 +41,9 @@ class HashSetMultiple extends Command
$flattenedKVs[] = $v;
}
- return $flattenedKVs;
+ $arguments = $flattenedKVs;
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php
new file mode 100644
index 000000000..62ce7dbe9
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php
@@ -0,0 +1,53 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hscan
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hscan
*/
-class HashScan extends Command
+class HSCAN extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,14 +30,14 @@ class HashScan extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 3 && is_array($arguments[2])) {
$options = $this->prepareOptions(array_pop($arguments));
$arguments = array_merge($arguments, $options);
}
- return $arguments;
+ parent::setArguments($arguments);
}
/**
@@ -49,7 +50,7 @@ class HashScan extends Command
protected function prepareOptions($options)
{
$options = array_change_key_case($options, CASE_UPPER);
- $normalized = array();
+ $normalized = [];
if (!empty($options['MATCH'])) {
$normalized[] = 'MATCH';
@@ -71,7 +72,7 @@ class HashScan extends Command
{
if (is_array($data)) {
$fields = $data[1];
- $result = array();
+ $result = [];
for ($i = 0; $i < count($fields); ++$i) {
$result[$fields[$i]] = $fields[++$i];
diff --git a/plugins/cache-redis/Predis/Command/Redis/HSET.php b/plugins/cache-redis/Predis/Command/Redis/HSET.php
new file mode 100644
index 000000000..662094a3c
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/HSET.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hstrlen
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hstrlen
*/
-class HashStringLength extends Command
+class HSTRLEN extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashValues.php b/plugins/cache-redis/Predis/Command/Redis/HVALS.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashValues.php
rename to plugins/cache-redis/Predis/Command/Redis/HVALS.php
index 0a5ea5f61..71172c709 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashValues.php
+++ b/plugins/cache-redis/Predis/Command/Redis/HVALS.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/hvals
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/hvals
*/
-class HashValues extends Command
+class HVALS extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionDiscard.php b/plugins/cache-redis/Predis/Command/Redis/INCR.php
similarity index 59%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionDiscard.php
rename to plugins/cache-redis/Predis/Command/Redis/INCR.php
index 44aca2b11..80bc1ee0e 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionDiscard.php
+++ b/plugins/cache-redis/Predis/Command/Redis/INCR.php
@@ -3,26 +3,27 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/discard
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/incr
*/
-class TransactionDiscard extends Command
+class INCR extends RedisCommand
{
/**
* {@inheritdoc}
*/
public function getId()
{
- return 'DISCARD';
+ return 'INCR';
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementBy.php b/plugins/cache-redis/Predis/Command/Redis/INCRBY.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementBy.php
rename to plugins/cache-redis/Predis/Command/Redis/INCRBY.php
index 9d8241a25..f4ce4e32c 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementBy.php
+++ b/plugins/cache-redis/Predis/Command/Redis/INCRBY.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/incrby
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/incrby
*/
-class StringIncrementBy extends Command
+class INCRBY extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementByFloat.php b/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementByFloat.php
rename to plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php
index 164a0869b..fb626edbe 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementByFloat.php
+++ b/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/incrbyfloat
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/incrbyfloat
*/
-class StringIncrementByFloat extends Command
+class INCRBYFLOAT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/INFO.php b/plugins/cache-redis/Predis/Command/Redis/INFO.php
new file mode 100644
index 000000000..26a62c2a3
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/INFO.php
@@ -0,0 +1,157 @@
+parseNewResponseFormat($lines);
+ } else {
+ return $this->parseOldResponseFormat($lines);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseNewResponseFormat($lines)
+ {
+ $info = [];
+ $current = null;
+
+ foreach ($lines as $row) {
+ if ($row === '') {
+ continue;
+ }
+
+ if (preg_match('/^# (\w+)$/', $row, $matches)) {
+ $info[$matches[1]] = [];
+ $current = &$info[$matches[1]];
+ continue;
+ }
+
+ [$k, $v] = $this->parseRow($row);
+ $current[$k] = $v;
+ }
+
+ return $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseOldResponseFormat($lines)
+ {
+ $info = [];
+
+ foreach ($lines as $row) {
+ if (strpos($row, ':') === false) {
+ continue;
+ }
+
+ [$k, $v] = $this->parseRow($row);
+ $info[$k] = $v;
+ }
+
+ return $info;
+ }
+
+ /**
+ * Parses a single row of the response and returns the key-value pair.
+ *
+ * @param string $row Single row of the response.
+ *
+ * @return array
+ */
+ protected function parseRow($row)
+ {
+ if (preg_match('/^module:name/', $row)) {
+ return $this->parseModuleRow($row);
+ }
+
+ [$k, $v] = explode(':', $row, 2);
+
+ if (preg_match('/^db\d+$/', $k)) {
+ $v = $this->parseDatabaseStats($v);
+ }
+
+ return [$k, $v];
+ }
+
+ /**
+ * Extracts the statistics of each logical DB from the string buffer.
+ *
+ * @param string $str Response buffer.
+ *
+ * @return array
+ */
+ protected function parseDatabaseStats($str)
+ {
+ $db = [];
+
+ foreach (explode(',', $str) as $dbvar) {
+ [$dbvk, $dbvv] = explode('=', $dbvar);
+ $db[trim($dbvk)] = $dbvv;
+ }
+
+ return $db;
+ }
+
+ /**
+ * Parsing module rows because of different format.
+ *
+ * @param string $row
+ * @return array
+ */
+ protected function parseModuleRow(string $row): array
+ {
+ [$moduleKeyword, $moduleData] = explode(':', $row);
+ $explodedData = explode(',', $moduleData);
+ $parsedData = [];
+
+ foreach ($explodedData as $moduleDataRow) {
+ [$k, $v] = explode('=', $moduleDataRow);
+
+ if ($k === 'name') {
+ $parsedData[0] = $v;
+ continue;
+ }
+
+ $parsedData[1][$k] = $v;
+ }
+
+ return $parsedData;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php
new file mode 100644
index 000000000..1b2a92036
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php
@@ -0,0 +1,28 @@
+setSpace($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setNewline($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setIndent($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php
new file mode 100644
index 000000000..a13222833
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php
@@ -0,0 +1,29 @@
+setSubcommand($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php
new file mode 100644
index 000000000..9b11458e4
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php
@@ -0,0 +1,28 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/keys
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/keys
*/
-class KeyKeys extends Command
+class KEYS extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerLastSave.php b/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerLastSave.php
rename to plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php
index feeb19a8a..0672845d2 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerLastSave.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lastsave
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lastsave
*/
-class ServerLastSave extends Command
+class LASTSAVE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/LCS.php b/plugins/cache-redis/Predis/Command/Redis/LCS.php
new file mode 100644
index 000000000..e8663f457
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/LCS.php
@@ -0,0 +1,69 @@
+filterArguments();
+ }
+
+ public function parseResponse($data)
+ {
+ if (is_array($data)) {
+ if ($data !== array_values($data)) {
+ return $data; // Relay
+ }
+
+ return [$data[0] => $data[1], $data[2] => $data[3]];
+ }
+
+ return $data;
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListIndex.php b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListIndex.php
rename to plugins/cache-redis/Predis/Command/Redis/LINDEX.php
index 27c64be73..80510e1e4 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListIndex.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lindex
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lindex
*/
-class ListIndex extends Command
+class LINDEX extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListInsert.php b/plugins/cache-redis/Predis/Command/Redis/LINSERT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListInsert.php
rename to plugins/cache-redis/Predis/Command/Redis/LINSERT.php
index 7d53d11b2..a81cf3414 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListInsert.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LINSERT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/linsert
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/linsert
*/
-class ListInsert extends Command
+class LINSERT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListLength.php b/plugins/cache-redis/Predis/Command/Redis/LLEN.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListLength.php
rename to plugins/cache-redis/Predis/Command/Redis/LLEN.php
index 6495beb77..628e60280 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListLength.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LLEN.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/llen
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/llen
*/
-class ListLength extends Command
+class LLEN extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/LMOVE.php b/plugins/cache-redis/Predis/Command/Redis/LMOVE.php
new file mode 100644
index 000000000..f7bad007d
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/LMOVE.php
@@ -0,0 +1,23 @@
+setCount($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setLeftRight($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setKeys($arguments);
+ $this->filterArguments();
+ }
+
+ public function parseResponse($data)
+ {
+ if (null === $data) {
+ return null;
+ }
+
+ return [$data[0] => $data[1]];
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopFirst.php b/plugins/cache-redis/Predis/Command/Redis/LPOP.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopFirst.php
rename to plugins/cache-redis/Predis/Command/Redis/LPOP.php
index 84d5d6734..d375bacaf 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopFirst.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LPOP.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lpop
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lpop
*/
-class ListPopFirst extends Command
+class LPOP extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/LPUSH.php b/plugins/cache-redis/Predis/Command/Redis/LPUSH.php
new file mode 100644
index 000000000..f3e0f9e73
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/LPUSH.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lpushx
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lpushx
*/
-class ListPushHeadX extends Command
+class LPUSHX extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRange.php b/plugins/cache-redis/Predis/Command/Redis/LRANGE.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListRange.php
rename to plugins/cache-redis/Predis/Command/Redis/LRANGE.php
index 32a21a6e6..4092dae73 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRange.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LRANGE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lrange
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lrange
*/
-class ListRange extends Command
+class LRANGE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRemove.php b/plugins/cache-redis/Predis/Command/Redis/LREM.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListRemove.php
rename to plugins/cache-redis/Predis/Command/Redis/LREM.php
index c5800899b..47e06c2fd 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRemove.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LREM.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lrem
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lrem
*/
-class ListRemove extends Command
+class LREM extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListSet.php b/plugins/cache-redis/Predis/Command/Redis/LSET.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListSet.php
rename to plugins/cache-redis/Predis/Command/Redis/LSET.php
index 5e59864de..255bb8976 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListSet.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LSET.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/lset
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/lset
*/
-class ListSet extends Command
+class LSET extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListTrim.php b/plugins/cache-redis/Predis/Command/Redis/LTRIM.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListTrim.php
rename to plugins/cache-redis/Predis/Command/Redis/LTRIM.php
index 193141809..c958ef6ec 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListTrim.php
+++ b/plugins/cache-redis/Predis/Command/Redis/LTRIM.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/ltrim
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/ltrim
*/
-class ListTrim extends Command
+class LTRIM extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/MGET.php b/plugins/cache-redis/Predis/Command/Redis/MGET.php
new file mode 100644
index 000000000..249cc43f8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/MGET.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/migrate
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/migrate
*/
-class KeyMigrate extends Command
+class MIGRATE extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,7 +30,7 @@ class KeyMigrate extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (is_array(end($arguments))) {
foreach (array_pop($arguments) as $modifier => $value) {
@@ -45,6 +46,6 @@ class KeyMigrate extends Command
}
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerMonitor.php b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerMonitor.php
rename to plugins/cache-redis/Predis/Command/Redis/MONITOR.php
index 1c3d33095..06e9e59b6 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerMonitor.php
+++ b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/monitor
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/monitor
*/
-class ServerMonitor extends Command
+class MONITOR extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/MOVE.php b/plugins/cache-redis/Predis/Command/Redis/MOVE.php
new file mode 100644
index 000000000..cd7a8e522
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/MOVE.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/mset
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/mset
*/
-class StringSetMultiple extends Command
+class MSET extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,10 +30,10 @@ class StringSetMultiple extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 1 && is_array($arguments[0])) {
- $flattenedKVs = array();
+ $flattenedKVs = [];
$args = $arguments[0];
foreach ($args as $k => $v) {
@@ -40,9 +41,9 @@ class StringSetMultiple extends Command
$flattenedKVs[] = $v;
}
- return $flattenedKVs;
+ $arguments = $flattenedKVs;
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyType.php b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php
similarity index 55%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyType.php
rename to plugins/cache-redis/Predis/Command/Redis/MSETNX.php
index f4f06e451..94d9ce269 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyType.php
+++ b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php
@@ -3,26 +3,25 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/type
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/msetnx
*/
-class KeyType extends Command
+class MSETNX extends MSET
{
/**
* {@inheritdoc}
*/
public function getId()
{
- return 'TYPE';
+ return 'MSETNX';
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionMulti.php b/plugins/cache-redis/Predis/Command/Redis/MULTI.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionMulti.php
rename to plugins/cache-redis/Predis/Command/Redis/MULTI.php
index 673bf55da..d8f96e64b 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionMulti.php
+++ b/plugins/cache-redis/Predis/Command/Redis/MULTI.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/multi
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/multi
*/
-class TransactionMulti extends Command
+class MULTI extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerObject.php b/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerObject.php
rename to plugins/cache-redis/Predis/Command/Redis/OBJECT_.php
index f921701c2..856a8740e 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerObject.php
+++ b/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/object
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/object
*/
-class ServerObject extends Command
+class OBJECT_ extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/PERSIST.php b/plugins/cache-redis/Predis/Command/Redis/PERSIST.php
new file mode 100644
index 000000000..8fd70e7aa
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/PERSIST.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/pexpire
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/pexpire
*/
-class KeyPreciseExpire extends KeyExpire
+class PEXPIRE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyPreciseExpireAt.php b/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyPreciseExpireAt.php
rename to plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php
index e41921870..6723a71e8 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyPreciseExpireAt.php
+++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/pexpireat
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/pexpireat
*/
-class KeyPreciseExpireAt extends KeyExpireAt
+class PEXPIREAT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php b/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php
new file mode 100644
index 000000000..de1289698
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/ping
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/ping
*/
-class ConnectionPing extends Command
+class PING extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringPreciseSetExpire.php b/plugins/cache-redis/Predis/Command/Redis/PSETEX.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringPreciseSetExpire.php
rename to plugins/cache-redis/Predis/Command/Redis/PSETEX.php
index 2faa954d0..3b8131e4a 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringPreciseSetExpire.php
+++ b/plugins/cache-redis/Predis/Command/Redis/PSETEX.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/psetex
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/psetex
*/
-class StringPreciseSetExpire extends StringSetExpire
+class PSETEX extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php
new file mode 100644
index 000000000..7377c7b16
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/pttl
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/pttl
*/
-class KeyPreciseTimeToLive extends KeyTimeToLive
+class PTTL extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPublish.php b/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPublish.php
rename to plugins/cache-redis/Predis/Command/Redis/PUBLISH.php
index 55508f8d9..d38cd1949 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPublish.php
+++ b/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/publish
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/publish
*/
-class PubSubPublish extends Command
+class PUBLISH extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPubsub.php b/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php
similarity index 80%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPubsub.php
rename to plugins/cache-redis/Predis/Command/Redis/PUBSUB.php
index 8cf812973..cd6396f5b 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPubsub.php
+++ b/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/pubsub
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/pubsub
*/
-class PubSubPubsub extends Command
+class PUBSUB extends RedisCommand
{
/**
* {@inheritdoc}
@@ -49,7 +50,7 @@ class PubSubPubsub extends Command
*/
protected static function processNumsub(array $channels)
{
- $processed = array();
+ $processed = [];
$count = count($channels);
for ($i = 0; $i < $count; ++$i) {
diff --git a/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php
new file mode 100644
index 000000000..f15a39fbb
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/quit
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/quit
*/
-class ConnectionQuit extends Command
+class QUIT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRandom.php b/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php
similarity index 66%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRandom.php
rename to plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php
index b208b2db4..c77e56243 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRandom.php
+++ b/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/randomkey
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/randomkey
*/
-class KeyRandom extends Command
+class RANDOMKEY extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRename.php b/plugins/cache-redis/Predis/Command/Redis/RENAME.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRename.php
rename to plugins/cache-redis/Predis/Command/Redis/RENAME.php
index 82e44fb2e..78a216ec0 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRename.php
+++ b/plugins/cache-redis/Predis/Command/Redis/RENAME.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/rename
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/rename
*/
-class KeyRename extends Command
+class RENAME extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php b/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php
new file mode 100644
index 000000000..ce1306fb7
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/restore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/restore
*/
-class KeyRestore extends Command
+class RESTORE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLast.php b/plugins/cache-redis/Predis/Command/Redis/RPOP.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLast.php
rename to plugins/cache-redis/Predis/Command/Redis/RPOP.php
index 9e92db5f4..3bc3f977f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLast.php
+++ b/plugins/cache-redis/Predis/Command/Redis/RPOP.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/rpop
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/rpop
*/
-class ListPopLast extends Command
+class RPOP extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLastPushHead.php b/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLastPushHead.php
rename to plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php
index f430eb227..d158f2d55 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLastPushHead.php
+++ b/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/rpoplpush
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/rpoplpush
*/
-class ListPopLastPushHead extends Command
+class RPOPLPUSH extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/RPUSH.php b/plugins/cache-redis/Predis/Command/Redis/RPUSH.php
new file mode 100644
index 000000000..a8a72f018
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/RPUSH.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/rpushx
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/rpushx
*/
-class ListPushTailX extends Command
+class RPUSHX extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SADD.php b/plugins/cache-redis/Predis/Command/Redis/SADD.php
new file mode 100644
index 000000000..e2213448b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SADD.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/save
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/save
*/
-class ServerSave extends Command
+class SAVE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyScan.php b/plugins/cache-redis/Predis/Command/Redis/SCAN.php
similarity index 77%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyScan.php
rename to plugins/cache-redis/Predis/Command/Redis/SCAN.php
index 05f5bb3ac..bf1da9625 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyScan.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SCAN.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/scan
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/scan
*/
-class KeyScan extends Command
+class SCAN extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,14 +30,14 @@ class KeyScan extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
$options = $this->prepareOptions(array_pop($arguments));
$arguments = array_merge($arguments, $options);
}
- return $arguments;
+ parent::setArguments($arguments);
}
/**
@@ -49,7 +50,7 @@ class KeyScan extends Command
protected function prepareOptions($options)
{
$options = array_change_key_case($options, CASE_UPPER);
- $normalized = array();
+ $normalized = [];
if (!empty($options['MATCH'])) {
$normalized[] = 'MATCH';
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetCardinality.php b/plugins/cache-redis/Predis/Command/Redis/SCARD.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetCardinality.php
rename to plugins/cache-redis/Predis/Command/Redis/SCARD.php
index a9f959b78..daf1393da 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetCardinality.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SCARD.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/scard
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/scard
*/
-class SetCardinality extends Command
+class SCARD extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerScript.php b/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerScript.php
rename to plugins/cache-redis/Predis/Command/Redis/SCRIPT.php
index 7a01018d9..5df20cf72 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerScript.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/script
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/script
*/
-class ServerScript extends Command
+class SCRIPT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SDIFF.php b/plugins/cache-redis/Predis/Command/Redis/SDIFF.php
new file mode 100644
index 000000000..b59e63df8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SDIFF.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/select
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/select
*/
-class ConnectionSelect extends Command
+class SELECT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSentinel.php b/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php
similarity index 73%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSentinel.php
rename to plugins/cache-redis/Predis/Command/Redis/SENTINEL.php
index c0962db3d..bd22ffba5 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSentinel.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/topics/sentinel
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/topics/sentinel
*/
-class ServerSentinel extends Command
+class SENTINEL extends RedisCommand
{
/**
* {@inheritdoc}
@@ -31,7 +32,10 @@ class ServerSentinel extends Command
*/
public function parseResponse($data)
{
- switch (strtolower($this->getArgument(0))) {
+ $argument = $this->getArgument(0);
+ $argument = is_null($argument) ? null : strtolower($argument);
+
+ switch ($argument) {
case 'masters':
case 'slaves':
return self::processMastersOrSlaves($data);
@@ -51,7 +55,7 @@ class ServerSentinel extends Command
protected static function processMastersOrSlaves(array $servers)
{
foreach ($servers as $idx => $node) {
- $processed = array();
+ $processed = [];
$count = count($node);
for ($i = 0; $i < $count; ++$i) {
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSet.php b/plugins/cache-redis/Predis/Command/Redis/SET.php
similarity index 59%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringSet.php
rename to plugins/cache-redis/Predis/Command/Redis/SET.php
index b1469945c..f8956f40b 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSet.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SET.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/set
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/set
*/
-class StringSet extends Command
+class SET extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetBit.php b/plugins/cache-redis/Predis/Command/Redis/SETBIT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetBit.php
rename to plugins/cache-redis/Predis/Command/Redis/SETBIT.php
index 7933b6be3..6c602ae1d 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetBit.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SETBIT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/setbit
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/setbit
*/
-class StringSetBit extends Command
+class SETBIT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetExpire.php b/plugins/cache-redis/Predis/Command/Redis/SETEX.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetExpire.php
rename to plugins/cache-redis/Predis/Command/Redis/SETEX.php
index f08817085..66189128f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetExpire.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SETEX.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/setex
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/setex
*/
-class StringSetExpire extends Command
+class SETEX extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SETNX.php b/plugins/cache-redis/Predis/Command/Redis/SETNX.php
new file mode 100644
index 000000000..d34796504
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SETNX.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/sdiff
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/setrange
*/
-class SetDifference extends SetIntersection
+class SETRANGE extends RedisCommand
{
/**
* {@inheritdoc}
*/
public function getId()
{
- return 'SDIFF';
+ return 'SETRANGE';
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php b/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php
new file mode 100644
index 000000000..4d2b74794
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php
@@ -0,0 +1,61 @@
+setLimit($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setKeys($arguments);
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetIntersectionStore.php b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php
similarity index 53%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetIntersectionStore.php
rename to plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php
index b748618aa..144335a1f 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetIntersectionStore.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/sinterstore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/sinterstore
*/
-class SetIntersectionStore extends Command
+class SINTERSTORE extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,12 +30,12 @@ class SetIntersectionStore extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 2 && is_array($arguments[1])) {
- return array_merge(array($arguments[0]), $arguments[1]);
+ $arguments = array_merge([$arguments[0]], $arguments[1]);
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php
new file mode 100644
index 000000000..3991c4c9e
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/slaveof
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/slaveof
*/
-class ServerSlaveOf extends Command
+class SLAVEOF extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,12 +30,12 @@ class ServerSlaveOf extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 0 || $arguments[0] === 'NO ONE') {
- return array('NO', 'ONE');
+ $arguments = ['NO', 'ONE'];
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSlowlog.php b/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php
similarity index 71%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSlowlog.php
rename to plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php
index 137ff59e7..41fbbcdc4 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSlowlog.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/slowlog
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/slowlog
*/
-class ServerSlowlog extends Command
+class SLOWLOG extends RedisCommand
{
/**
* {@inheritdoc}
@@ -32,15 +33,15 @@ class ServerSlowlog extends Command
public function parseResponse($data)
{
if (is_array($data)) {
- $log = array();
+ $log = [];
foreach ($data as $index => $entry) {
- $log[$index] = array(
+ $log[$index] = [
'id' => $entry[0],
'timestamp' => $entry[1],
'duration' => $entry[2],
'command' => $entry[3],
- );
+ ];
}
return $log;
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetMembers.php b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetMembers.php
rename to plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php
index f4076ae8b..8f32be40e 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetMembers.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/smembers
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/smembers
*/
-class SetMembers extends Command
+class SMEMBERS extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php
new file mode 100644
index 000000000..735b01d4f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php
@@ -0,0 +1,28 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/sort
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/sort
*/
-class KeySort extends Command
+class SORT extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,13 +30,15 @@ class KeySort extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 1) {
- return $arguments;
+ parent::setArguments($arguments);
+
+ return;
}
- $query = array($arguments[0]);
+ $query = [$arguments[0]];
$sortParams = array_change_key_case($arguments[1], CASE_UPPER);
if (isset($sortParams['BY'])) {
@@ -57,9 +60,9 @@ class KeySort extends Command
}
}
- if (isset($sortParams['LIMIT']) &&
- is_array($sortParams['LIMIT']) &&
- count($sortParams['LIMIT']) == 2) {
+ if (isset($sortParams['LIMIT'])
+ && is_array($sortParams['LIMIT'])
+ && count($sortParams['LIMIT']) == 2) {
$query[] = 'LIMIT';
$query[] = $sortParams['LIMIT'][0];
$query[] = $sortParams['LIMIT'][1];
@@ -78,6 +81,6 @@ class KeySort extends Command
$query[] = $sortParams['STORE'];
}
- return $query;
+ parent::setArguments($query);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php b/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php
new file mode 100644
index 000000000..f302b6e3b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php
@@ -0,0 +1,74 @@
+setSorting($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setGetArgument($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setLimit($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setBy($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetPop.php b/plugins/cache-redis/Predis/Command/Redis/SPOP.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetPop.php
rename to plugins/cache-redis/Predis/Command/Redis/SPOP.php
index b78d3f33b..e09a3d55a 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetPop.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SPOP.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/spop
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/spop
*/
-class SetPop extends Command
+class SPOP extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetRandomMember.php b/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetRandomMember.php
rename to plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php
index 2cb79a049..61d8f03b5 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetRandomMember.php
+++ b/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/srandmember
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/srandmember
*/
-class SetRandomMember extends Command
+class SRANDMEMBER extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SREM.php b/plugins/cache-redis/Predis/Command/Redis/SREM.php
new file mode 100644
index 000000000..ad3cf062f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SREM.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/sscan
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/sscan
*/
-class SetScan extends Command
+class SSCAN extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,14 +30,14 @@ class SetScan extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 3 && is_array($arguments[2])) {
$options = $this->prepareOptions(array_pop($arguments));
$arguments = array_merge($arguments, $options);
}
- return $arguments;
+ parent::setArguments($arguments);
}
/**
@@ -49,7 +50,7 @@ class SetScan extends Command
protected function prepareOptions($options)
{
$options = array_change_key_case($options, CASE_UPPER);
- $normalized = array();
+ $normalized = [];
if (!empty($options['MATCH'])) {
$normalized[] = 'MATCH';
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringStrlen.php b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringStrlen.php
rename to plugins/cache-redis/Predis/Command/Redis/STRLEN.php
index 10f492fd9..f51775662 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringStrlen.php
+++ b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/strlen
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/strlen
*/
-class StringStrlen extends Command
+class STRLEN extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php
new file mode 100644
index 000000000..b133c2252
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/substr
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/substr
*/
-class StringSubstr extends Command
+class SUBSTR extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/SUNION.php b/plugins/cache-redis/Predis/Command/Redis/SUNION.php
new file mode 100644
index 000000000..7a94e8da2
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/SUNION.php
@@ -0,0 +1,39 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$index, $query],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php
new file mode 100644
index 000000000..a2511ea32
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php
@@ -0,0 +1,28 @@
+toArray() : [];
+
+ $schema = array_reduce($schema, static function (array $carry, FieldInterface $field) {
+ return array_merge($carry, $field->toArray());
+ }, []);
+
+ array_unshift($schema, 'SCHEMA', 'ADD');
+
+ parent::setArguments(array_merge(
+ [$index],
+ $commandArguments,
+ $schema
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php
new file mode 100644
index 000000000..9cba60e05
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php
@@ -0,0 +1,30 @@
+toArray() : [];
+
+ $schema = array_reduce($schema, static function (array $carry, FieldInterface $field) {
+ return array_merge($carry, $field->toArray());
+ }, []);
+
+ array_unshift($schema, 'SCHEMA');
+
+ parent::setArguments(array_merge(
+ [$index],
+ $commandArguments,
+ $schema
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php
new file mode 100644
index 000000000..14a01948a
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php
@@ -0,0 +1,34 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$subcommand, $index, $cursorId],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php
new file mode 100644
index 000000000..c0bc3da72
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php
@@ -0,0 +1,28 @@
+toArray();
+ }
+
+ parent::setArguments(array_merge(
+ [$index],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php
new file mode 100644
index 000000000..e2aa35d47
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php
@@ -0,0 +1,43 @@
+toArray();
+ }
+
+ parent::setArguments(array_merge(
+ [$index, $query],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php
new file mode 100644
index 000000000..02fa95974
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php
@@ -0,0 +1,28 @@
+toArray()
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php
new file mode 100644
index 000000000..9fbe8a563
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php
@@ -0,0 +1,39 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$index, $query],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php
new file mode 100644
index 000000000..ac0232bb2
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php
@@ -0,0 +1,38 @@
+toArray();
+ }
+
+ parent::setArguments(array_merge(
+ [$index, $query],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php
new file mode 100644
index 000000000..71c42a4a3
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php
@@ -0,0 +1,39 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key, $string, $score],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php
new file mode 100644
index 000000000..15457d10b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php
@@ -0,0 +1,28 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key, $prefix],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php
new file mode 100644
index 000000000..1e11d8d30
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php
@@ -0,0 +1,28 @@
+toArray();
+ }
+
+ $terms = array_slice($arguments, 3);
+
+ parent::setArguments(array_merge(
+ [$index, $synonymGroupId],
+ $commandArguments,
+ $terms
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php
new file mode 100644
index 000000000..b323949fe
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php
@@ -0,0 +1,28 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/time
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/time
*/
-class ServerTime extends Command
+class TIME extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TOUCH.php b/plugins/cache-redis/Predis/Command/Redis/TOUCH.php
new file mode 100644
index 000000000..82c13fef8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TOUCH.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/ttl
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/ttl
*/
-class KeyTimeToLive extends Command
+class TTL extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TYPE.php b/plugins/cache-redis/Predis/Command/Redis/TYPE.php
new file mode 100644
index 000000000..dece63778
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TYPE.php
@@ -0,0 +1,51 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key, $timestamp, $value],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php
new file mode 100644
index 000000000..8797d68a3
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php
@@ -0,0 +1,39 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php
new file mode 100644
index 000000000..0d88072c0
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php
@@ -0,0 +1,39 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php
new file mode 100644
index 000000000..c8bd62bcb
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php
@@ -0,0 +1,40 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key, $value],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php
new file mode 100644
index 000000000..747c5a629
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php
@@ -0,0 +1,28 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php
new file mode 100644
index 000000000..3141e5ded
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php
@@ -0,0 +1,41 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key, $value],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php
new file mode 100644
index 000000000..d4941d407
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php
@@ -0,0 +1,39 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php
new file mode 100644
index 000000000..085224693
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php
@@ -0,0 +1,28 @@
+toArray();
+
+ array_push($processedArguments, 'FILTER', ...$arguments);
+
+ parent::setArguments(array_merge(
+ $commandArguments,
+ $processedArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php
new file mode 100644
index 000000000..3d68cdd99
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php
@@ -0,0 +1,39 @@
+toArray();
+
+ parent::setArguments(array_merge(
+ [$fromTimestamp, $toTimestamp],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php
new file mode 100644
index 000000000..987fd8201
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php
@@ -0,0 +1,26 @@
+toArray() : [];
+
+ parent::setArguments(array_merge(
+ [$key, $fromTimestamp, $toTimestamp],
+ $commandArguments
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php
new file mode 100644
index 000000000..1e8768e96
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php
@@ -0,0 +1,26 @@
+filterArguments();
+ }
+
+ public function parseResponse($data)
+ {
+ if ($this->isWithCountModifier()) {
+ $result = [];
+
+ for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) {
+ if (array_key_exists($i + 1, $data)) {
+ $result[(string) $data[$i]] = $data[++$i];
+ }
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Checks for the presence of the WITHCOUNT modifier.
+ *
+ * @return bool
+ */
+ private function isWithCountModifier(): bool
+ {
+ $arguments = $this->getArguments();
+ $lastArgument = (!empty($arguments)) ? $arguments[count($arguments) - 1] : null;
+
+ return is_string($lastArgument) && strtoupper($lastArgument) === 'WITHCOUNT';
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php
new file mode 100644
index 000000000..128902194
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/unwatch
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/unwatch
*/
-class TransactionUnwatch extends Command
+class UNWATCH extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php b/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php
new file mode 100644
index 000000000..f58cc0547
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php
@@ -0,0 +1,29 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/watch
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/watch
*/
-class TransactionWatch extends Command
+class WATCH extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,12 +30,12 @@ class TransactionWatch extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (isset($arguments[0]) && is_array($arguments[0])) {
- return $arguments[0];
+ $arguments = $arguments[0];
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/plugins/cache-redis/Predis/Command/Redis/XADD.php b/plugins/cache-redis/Predis/Command/Redis/XADD.php
new file mode 100644
index 000000000..21c132272
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/XADD.php
@@ -0,0 +1,64 @@
+ $val) {
+ $args[] = $key;
+ $args[] = $val;
+ }
+ }
+
+ parent::setArguments($args);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/XDEL.php b/plugins/cache-redis/Predis/Command/Redis/XDEL.php
new file mode 100644
index 000000000..f1c509e35
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/XDEL.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zadd
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zadd
*/
-class ZSetAdd extends Command
+class ZADD extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,7 +30,7 @@ class ZSetAdd extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (is_array(end($arguments))) {
foreach (array_pop($arguments) as $member => $score) {
@@ -38,6 +39,6 @@ class ZSetAdd extends Command
}
}
- return $arguments;
+ parent::setArguments($arguments);
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCardinality.php b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCardinality.php
rename to plugins/cache-redis/Predis/Command/Redis/ZCARD.php
index 10332009a..7f6cfd4d0 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCardinality.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zcard
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zcard
*/
-class ZSetCardinality extends Command
+class ZCARD extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCount.php b/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCount.php
rename to plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php
index 918bd2b80..32b54cb33 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCount.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zcount
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zcount
*/
-class ZSetCount extends Command
+class ZCOUNT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php b/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php
new file mode 100644
index 000000000..573dcae68
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php
@@ -0,0 +1,48 @@
+setKeys($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setWithScore($arguments);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php
new file mode 100644
index 000000000..729988372
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php
@@ -0,0 +1,40 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zincrby
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zincrby
*/
-class ZSetIncrementBy extends Command
+class ZINCRBY extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZINTER.php b/plugins/cache-redis/Predis/Command/Redis/ZINTER.php
new file mode 100644
index 000000000..787db2bbe
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZINTER.php
@@ -0,0 +1,35 @@
+setLimit($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setKeys($arguments);
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetIntersectionStore.php b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetIntersectionStore.php
rename to plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php
index 572a7a324..7f0aa4498 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetIntersectionStore.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php
@@ -3,20 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/zinterstore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zinterstore
*/
-class ZSetIntersectionStore extends ZSetUnionStore
+class ZINTERSTORE extends ZUNIONSTORE
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetLexCount.php b/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetLexCount.php
rename to plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php
index 447b8eb32..c216b99d9 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetLexCount.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zlexcount
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zlexcount
*/
-class ZSetLexCount extends Command
+class ZLEXCOUNT extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php b/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php
new file mode 100644
index 000000000..4adc0cca9
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php
@@ -0,0 +1,79 @@
+setCount($arguments);
+ $arguments = $this->getArguments();
+
+ $this->resolveModifier(static::$modifierArgumentPositionOffset, $arguments);
+
+ $this->setKeys($arguments);
+ $arguments = $this->getArguments();
+
+ parent::setArguments($arguments);
+ }
+
+ public function parseResponse($data)
+ {
+ $key = array_shift($data);
+
+ if (null === $key) {
+ return [$key];
+ }
+
+ $data = $data[0];
+ $parsedData = [];
+
+ for ($i = 0, $iMax = count($data); $i < $iMax; $i++) {
+ for ($j = 0, $jMax = count($data[$i]); $j < $jMax; ++$j) {
+ if ($data[$i][$j + 1] ?? false) {
+ $parsedData[$data[$i][$j]] = $data[$i][++$j];
+ }
+ }
+ }
+
+ return array_combine([$key], [$parsedData]);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php
new file mode 100644
index 000000000..2dd76fd65
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php
@@ -0,0 +1,34 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zrange
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrange
*/
-class ZSetRange extends Command
+class ZRANGE extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,25 +30,24 @@ class ZSetRange extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 4) {
$lastType = gettype($arguments[3]);
if ($lastType === 'string' && strtoupper($arguments[3]) === 'WITHSCORES') {
// Used for compatibility with older versions
- $arguments[3] = array('WITHSCORES' => true);
+ $arguments[3] = ['WITHSCORES' => true];
$lastType = 'array';
}
if ($lastType === 'array') {
$options = $this->prepareOptions(array_pop($arguments));
-
- return array_merge($arguments, $options);
+ $arguments = array_merge($arguments, $options);
}
}
- return $arguments;
+ parent::setArguments($arguments);
}
/**
@@ -60,7 +60,7 @@ class ZSetRange extends Command
protected function prepareOptions($options)
{
$opts = array_change_key_case($options, CASE_UPPER);
- $finalizedOpts = array();
+ $finalizedOpts = [];
if (!empty($opts['WITHSCORES'])) {
$finalizedOpts[] = 'WITHSCORES';
@@ -91,10 +91,14 @@ class ZSetRange extends Command
public function parseResponse($data)
{
if ($this->withScores()) {
- $result = array();
+ $result = [];
for ($i = 0; $i < count($data); ++$i) {
- $result[$data[$i]] = $data[++$i];
+ if (is_array($data[$i])) {
+ $result[$data[$i][0]] = $data[$i][1]; // Relay
+ } else {
+ $result[$data[$i]] = $data[++$i];
+ }
}
return $result;
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByLex.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php
similarity index 65%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByLex.php
rename to plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php
index 9b2991a81..18b4a6d30 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByLex.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php
@@ -3,20 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/zrangebylex
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrangebylex
*/
-class ZSetRangeByLex extends ZSetRange
+class ZRANGEBYLEX extends ZRANGE
{
/**
* {@inheritdoc}
@@ -32,14 +31,14 @@ class ZSetRangeByLex extends ZSetRange
protected function prepareOptions($options)
{
$opts = array_change_key_case($options, CASE_UPPER);
- $finalizedOpts = array();
+ $finalizedOpts = [];
if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
$limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);
$finalizedOpts[] = 'LIMIT';
- $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
- $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
+ $finalizedOpts[] = $limit['OFFSET'] ?? $limit[0];
+ $finalizedOpts[] = $limit['COUNT'] ?? $limit[1];
}
return $finalizedOpts;
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByScore.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php
similarity index 73%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByScore.php
rename to plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php
index 961a5bc2e..66cbe4eab 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByScore.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php
@@ -3,20 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/zrangebyscore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrangebyscore
*/
-class ZSetRangeByScore extends ZSetRange
+class ZRANGEBYSCORE extends ZRANGE
{
/**
* {@inheritdoc}
@@ -32,14 +31,14 @@ class ZSetRangeByScore extends ZSetRange
protected function prepareOptions($options)
{
$opts = array_change_key_case($options, CASE_UPPER);
- $finalizedOpts = array();
+ $finalizedOpts = [];
if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
$limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);
$finalizedOpts[] = 'LIMIT';
- $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
- $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
+ $finalizedOpts[] = $limit['OFFSET'] ?? $limit[0];
+ $finalizedOpts[] = $limit['COUNT'] ?? $limit[1];
}
return array_merge($finalizedOpts, parent::prepareOptions($options));
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php
new file mode 100644
index 000000000..4f820b06c
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php
@@ -0,0 +1,57 @@
+setByLexByScoreArgument($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setReversedArgument($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setLimitArguments($arguments);
+ $this->filterArguments();
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRank.php b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRank.php
rename to plugins/cache-redis/Predis/Command/Redis/ZRANK.php
index d0c9c536e..3c6ac2a6e 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRank.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zrank
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrank
*/
-class ZSetRank extends Command
+class ZRANK extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREM.php b/plugins/cache-redis/Predis/Command/Redis/ZREM.php
new file mode 100644
index 000000000..f14a0790a
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREM.php
@@ -0,0 +1,39 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zremrangebylex
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zremrangebylex
*/
-class ZSetRemoveRangeByLex extends Command
+class ZREMRANGEBYLEX extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByRank.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByRank.php
rename to plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php
index 89cd5baff..3aeff30c0 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByRank.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zremrangebyrank
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zremrangebyrank
*/
-class ZSetRemoveRangeByRank extends Command
+class ZREMRANGEBYRANK extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByScore.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByScore.php
rename to plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php
index a7c30814b..6f88c5d1d 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByScore.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zremrangebyscore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zremrangebyscore
*/
-class ZSetRemoveRangeByScore extends Command
+class ZREMRANGEBYSCORE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRange.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRange.php
rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php
index 6a46a7a5a..a22566191 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRange.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php
@@ -3,20 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/zrevrange
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrevrange
*/
-class ZSetReverseRange extends ZSetRange
+class ZREVRANGE extends ZRANGE
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByLex.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php
similarity index 61%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByLex.php
rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php
index cdd8ba623..75cad0bce 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByLex.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php
@@ -3,15 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
-class ZSetReverseRangeByLex extends ZSetRangeByLex
+/**
+ * @see http://redis.io/commands/zrevrangebylex
+ */
+class ZREVRANGEBYLEX extends ZRANGEBYLEX
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByScore.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php
similarity index 57%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByScore.php
rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php
index 1078eb72b..9acc450df 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByScore.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php
@@ -3,20 +3,19 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
/**
- * @link http://redis.io/commands/zrevrangebyscore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrevrangebyscore
*/
-class ZSetReverseRangeByScore extends ZSetRangeByScore
+class ZREVRANGEBYSCORE extends ZRANGEBYSCORE
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRank.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRank.php
rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php
index 33fb81584..620d2fe70 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRank.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zrevrank
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zrevrank
*/
-class ZSetReverseRank extends Command
+class ZREVRANK extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScan.php b/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php
similarity index 80%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScan.php
rename to plugins/cache-redis/Predis/Command/Redis/ZSCAN.php
index 1dc2352ed..ee0b13105 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScan.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zscan
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zscan
*/
-class ZSetScan extends Command
+class ZSCAN extends RedisCommand
{
/**
* {@inheritdoc}
@@ -29,14 +30,14 @@ class ZSetScan extends Command
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (count($arguments) === 3 && is_array($arguments[2])) {
$options = $this->prepareOptions(array_pop($arguments));
$arguments = array_merge($arguments, $options);
}
- return $arguments;
+ parent::setArguments($arguments);
}
/**
@@ -49,7 +50,7 @@ class ZSetScan extends Command
protected function prepareOptions($options)
{
$options = array_change_key_case($options, CASE_UPPER);
- $normalized = array();
+ $normalized = [];
if (!empty($options['MATCH'])) {
$normalized[] = 'MATCH';
@@ -71,7 +72,7 @@ class ZSetScan extends Command
{
if (is_array($data)) {
$members = $data[1];
- $result = array();
+ $result = [];
for ($i = 0; $i < count($members); ++$i) {
$result[$members[$i]] = (float) $members[++$i];
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScore.php b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php
similarity index 58%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScore.php
rename to plugins/cache-redis/Predis/Command/Redis/ZSCORE.php
index 2e7fce8ed..978a17292 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScore.php
+++ b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php
@@ -3,20 +3,21 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Command;
+namespace Predis\Command\Redis;
+
+use Predis\Command\Command as RedisCommand;
/**
- * @link http://redis.io/commands/zscore
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/zscore
*/
-class ZSetScore extends Command
+class ZSCORE extends RedisCommand
{
/**
* {@inheritdoc}
diff --git a/plugins/cache-redis/Predis/Command/Redis/ZUNION.php b/plugins/cache-redis/Predis/Command/Redis/ZUNION.php
new file mode 100644
index 000000000..e0e7f233f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Redis/ZUNION.php
@@ -0,0 +1,35 @@
+setAggregate($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setWeights($arguments);
+ $arguments = $this->getArguments();
+
+ $this->setKeys($arguments);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/RedisFactory.php b/plugins/cache-redis/Predis/Command/RedisFactory.php
new file mode 100644
index 000000000..10c4541d5
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/RedisFactory.php
@@ -0,0 +1,112 @@
+commands = [
+ 'ECHO' => 'Predis\Command\Redis\ECHO_',
+ 'EVAL' => 'Predis\Command\Redis\EVAL_',
+ 'OBJECT' => 'Predis\Command\Redis\OBJECT_',
+ // Class name corresponds to PHP reserved word "function", added mapping to bypass restrictions
+ 'FUNCTION' => FUNCTIONS::class,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCommandClass(string $commandID): ?string
+ {
+ $commandID = strtoupper($commandID);
+
+ if (isset($this->commands[$commandID]) || array_key_exists($commandID, $this->commands)) {
+ return $this->commands[$commandID];
+ }
+
+ $commandClass = $this->resolve($commandID);
+
+ if (null === $commandClass) {
+ return null;
+ }
+
+ $this->commands[$commandID] = $commandClass;
+
+ return $commandClass;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function undefine(string $commandID): void
+ {
+ // NOTE: we explicitly associate `NULL` to the command ID in the map
+ // instead of the parent's `unset()` because our subclass tries to load
+ // a predefined class from the Predis\Command\Redis namespace when no
+ // explicit mapping is defined, see RedisFactory::getCommandClass() for
+ // details of the implementation of this mechanism.
+ $this->commands[strtoupper($commandID)] = null;
+ }
+
+ /**
+ * Resolves command object from given command ID.
+ *
+ * @param string $commandID Command ID of virtual method call
+ * @return string|null FQDN of corresponding command object
+ */
+ private function resolve(string $commandID): ?string
+ {
+ if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandID)) {
+ return $commandClass;
+ }
+
+ $commandModule = $this->resolveCommandModuleByPrefix($commandID);
+
+ if (null === $commandModule) {
+ return null;
+ }
+
+ if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandModule . '\\' . $commandID)) {
+ return $commandClass;
+ }
+
+ return null;
+ }
+
+ private function resolveCommandModuleByPrefix(string $commandID): ?string
+ {
+ foreach (ClientConfiguration::getModules() as $module) {
+ if (preg_match("/^{$module['commandPrefix']}/", $commandID)) {
+ return $module['name'];
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ScriptCommand.php b/plugins/cache-redis/Predis/Command/ScriptCommand.php
similarity index 62%
rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ScriptCommand.php
rename to plugins/cache-redis/Predis/Command/ScriptCommand.php
index a30bc1d28..330ee94b3 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ScriptCommand.php
+++ b/plugins/cache-redis/Predis/Command/ScriptCommand.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -15,12 +16,18 @@ namespace Predis\Command;
* Base class used to implement an higher level abstraction for commands based
* on Lua scripting with EVAL and EVALSHA.
*
- * @link http://redis.io/commands/eval
- *
- * @author Daniele Alessandri
+ * @see http://redis.io/commands/eval
*/
-abstract class ScriptCommand extends ServerEvalSHA
+abstract class ScriptCommand extends Command
{
+ /**
+ * {@inheritdoc}
+ */
+ public function getId()
+ {
+ return 'EVALSHA';
+ }
+
/**
* Gets the body of a Lua script.
*
@@ -28,6 +35,16 @@ abstract class ScriptCommand extends ServerEvalSHA
*/
abstract public function getScript();
+ /**
+ * Calculates the SHA1 hash of the body of the script.
+ *
+ * @return string SHA1 hash.
+ */
+ public function getScriptHash()
+ {
+ return sha1($this->getScript());
+ }
+
/**
* Specifies the number of arguments that should be considered as keys.
*
@@ -55,16 +72,20 @@ abstract class ScriptCommand extends ServerEvalSHA
/**
* {@inheritdoc}
*/
- protected function filterArguments(array $arguments)
+ public function setArguments(array $arguments)
{
if (($numkeys = $this->getKeysCount()) && $numkeys < 0) {
$numkeys = count($arguments) + $numkeys;
}
- return array_merge(array(sha1($this->getScript()), (int) $numkeys), $arguments);
+ $arguments = array_merge([$this->getScriptHash(), (int) $numkeys], $arguments);
+
+ parent::setArguments($arguments);
}
/**
+ * Returns arguments for EVAL command.
+ *
* @return array
*/
public function getEvalArguments()
@@ -74,4 +95,14 @@ abstract class ScriptCommand extends ServerEvalSHA
return $arguments;
}
+
+ /**
+ * Returns the equivalent EVAL command as a raw command instance.
+ *
+ * @return RawCommand
+ */
+ public function getEvalCommand()
+ {
+ return new RawCommand('EVAL', $this->getEvalArguments());
+ }
}
diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php
new file mode 100644
index 000000000..250ae744e
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php
@@ -0,0 +1,26 @@
+separator = $separator;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function resolve(string $commandId, string $subcommandId): SubcommandStrategyInterface
+ {
+ $subcommandStrategyClass = ucwords($subcommandId) . 'Strategy';
+ $commandDirectoryName = ucwords($commandId);
+
+ if (!is_null($this->separator)) {
+ $subcommandStrategyClass = str_replace($this->separator, '', $subcommandStrategyClass);
+ $commandDirectoryName = str_replace($this->separator, '', $commandDirectoryName);
+ }
+
+ if (class_exists(
+ $containerCommandClass = self::CONTAINER_COMMANDS_NAMESPACE . '\\' . $commandDirectoryName . '\\' . $subcommandStrategyClass
+ )) {
+ return new $containerCommandClass();
+ }
+
+ throw new InvalidArgumentException('Non-existing container command given');
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Aggregate.php b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php
new file mode 100644
index 000000000..c49c31085
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php
@@ -0,0 +1,66 @@
+ 'MIN',
+ 'max' => 'MAX',
+ 'sum' => 'SUM',
+ ];
+
+ /**
+ * @var string
+ */
+ private static $aggregateModifier = 'AGGREGATE';
+
+ public function setArguments(array $arguments)
+ {
+ $argumentsLength = count($arguments);
+
+ if (static::$aggregateArgumentPositionOffset >= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$aggregateArgumentPositionOffset];
+
+ if (is_string($argument) && in_array(strtoupper($argument), self::$aggregateValuesEnum)) {
+ $argument = self::$aggregateValuesEnum[$argument];
+ } else {
+ $enumValues = implode(', ', array_keys(self::$aggregateValuesEnum));
+ throw new UnexpectedValueException("Aggregate argument accepts only: {$enumValues} values");
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$aggregateArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$aggregateArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$aggregateModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BitByte.php b/plugins/cache-redis/Predis/Command/Traits/BitByte.php
new file mode 100644
index 000000000..067302d22
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BitByte.php
@@ -0,0 +1,40 @@
+ 'BIT',
+ 'byte' => 'BYTE',
+ ];
+
+ public function setArguments(array $arguments)
+ {
+ $value = array_pop($arguments);
+
+ if (null === $value) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (in_array(strtoupper($value), self::$argumentEnum, true)) {
+ $arguments[] = self::$argumentEnum[$value];
+ } else {
+ $arguments[] = $value;
+ }
+
+ parent::setArguments($arguments);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php
new file mode 100644
index 000000000..99e22be05
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php
@@ -0,0 +1,57 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$bucketSizeArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$bucketSizeArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$bucketSizeArgumentPositionOffset] < 1) {
+ throw new UnexpectedValueException('Wrong bucket size argument value or position offset');
+ }
+
+ $argument = $arguments[static::$bucketSizeArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$bucketSizeArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$bucketSizeArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$bucketSizeModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php
new file mode 100644
index 000000000..c0dccc8a4
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php
@@ -0,0 +1,57 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$capacityArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$capacityArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$capacityArgumentPositionOffset] < 1) {
+ throw new UnexpectedValueException('Wrong capacity argument value or position offset');
+ }
+
+ $argument = $arguments[static::$capacityArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$capacityArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$capacityArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$capacityModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php
new file mode 100644
index 000000000..661def80f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php
@@ -0,0 +1,57 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$errorArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$errorArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$errorArgumentPositionOffset] < 0) {
+ throw new UnexpectedValueException('Wrong error argument value or position offset');
+ }
+
+ $argument = $arguments[static::$errorArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$errorArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$errorArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$errorModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php
new file mode 100644
index 000000000..74d916f4c
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php
@@ -0,0 +1,53 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$expansionArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$expansionArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$expansionArgumentPositionOffset] < 1) {
+ throw new UnexpectedValueException('Wrong expansion argument value or position offset');
+ }
+
+ $argument = $arguments[static::$expansionArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$expansionArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$expansionArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$expansionModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php
new file mode 100644
index 000000000..9d2e4dcfe
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php
@@ -0,0 +1,45 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$itemsArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$itemsArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$itemsArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$itemsModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php
new file mode 100644
index 000000000..fb307e6d9
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php
@@ -0,0 +1,57 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$maxIterationsArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$maxIterationsArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$maxIterationsArgumentPositionOffset] < 1) {
+ throw new UnexpectedValueException('Wrong max iterations argument value or position offset');
+ }
+
+ $argument = $arguments[static::$maxIterationsArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$maxIterationsArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$maxIterationsArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$maxIterationsModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php
new file mode 100644
index 000000000..7fc084ec8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php
@@ -0,0 +1,49 @@
+= $argumentsLength
+ || false === $arguments[static::$noCreateArgumentPositionOffset]
+ ) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$noCreateArgumentPositionOffset];
+
+ if (true === $argument) {
+ $argument = 'NOCREATE';
+ } else {
+ throw new UnexpectedValueException('Wrong NOCREATE argument type');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$noCreateArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$noCreateArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php
new file mode 100644
index 000000000..99bd1722f
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php
@@ -0,0 +1,40 @@
+= $argumentsLength || null === $arguments[static::$byArgumentPositionOffset]) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$byArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$byArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$byArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$this->byModifier, $argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php
new file mode 100644
index 000000000..66c531584
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php
@@ -0,0 +1,49 @@
+ 'BYLEX',
+ 'byscore' => 'BYSCORE',
+ ];
+
+ public function setArguments(array $arguments)
+ {
+ $argument = $arguments[static::$byLexByScoreArgumentPositionOffset];
+
+ if (false === $argument) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (is_string($argument) && in_array(strtoupper($argument), self::$argumentsEnum)) {
+ $argument = self::$argumentsEnum[$argument];
+ } else {
+ throw new UnexpectedValueException('By argument accepts only "bylex" and "byscore" values');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$byLexByScoreArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$byLexByScoreArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php
new file mode 100644
index 000000000..c5ee2b7d1
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php
@@ -0,0 +1,49 @@
+getByArgumentPositionOffset($arguments);
+
+ if (null === $argumentPositionOffset) {
+ throw new InvalidArgumentException('Invalid BY argument value given');
+ }
+
+ $byArgumentObject = $arguments[$argumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ $byArgumentObject->toArray(),
+ $argumentsAfter
+ ));
+ }
+
+ private function getByArgumentPositionOffset(array $arguments): ?int
+ {
+ foreach ($arguments as $i => $value) {
+ if ($value instanceof ByInterface) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Count.php b/plugins/cache-redis/Predis/Command/Traits/Count.php
new file mode 100644
index 000000000..46ae489d6
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Count.php
@@ -0,0 +1,71 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$countArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$countArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$countArgumentPositionOffset] < 1) {
+ throw new UnexpectedValueException('Wrong count argument value or position offset');
+ }
+
+ $countArgument = $arguments[static::$countArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$countArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 2);
+
+ if (!$any) {
+ $argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 1);
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [$this->countModifier],
+ [$countArgument],
+ $argumentsAfter
+ ));
+
+ return;
+ }
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [$this->countModifier],
+ [$countArgument],
+ [$this->anyModifier],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/DB.php b/plugins/cache-redis/Predis/Command/Traits/DB.php
new file mode 100644
index 000000000..cf494f96a
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/DB.php
@@ -0,0 +1,53 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (!is_numeric($arguments[static::$dbArgumentPositionOffset])) {
+ throw new UnexpectedValueException('DB argument should be a valid numeric value');
+ }
+
+ if ($arguments[static::$dbArgumentPositionOffset] < 0) {
+ array_splice($arguments, static::$dbArgumentPositionOffset, 1);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$dbArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$dbArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$dbArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [$this->dbModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php
new file mode 100644
index 000000000..4f683f3ca
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php
@@ -0,0 +1,42 @@
+ 'NX',
+ 'xx' => 'XX',
+ 'gt' => 'GT',
+ 'lt' => 'LT',
+ ];
+
+ public function setArguments(array $arguments)
+ {
+ $value = array_pop($arguments);
+
+ if (null === $value) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (in_array(strtoupper($value), self::$argumentEnum, true)) {
+ $arguments[] = self::$argumentEnum[strtolower($value)];
+ } else {
+ $arguments[] = $value;
+ }
+
+ parent::setArguments($arguments);
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php
new file mode 100644
index 000000000..22688adba
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php
@@ -0,0 +1,49 @@
+getFromArgumentPositionOffset($arguments);
+
+ if (null === $argumentPositionOffset) {
+ throw new InvalidArgumentException('Invalid FROM argument value given');
+ }
+
+ $fromArgumentObject = $arguments[$argumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ $fromArgumentObject->toArray(),
+ $argumentsAfter
+ ));
+ }
+
+ private function getFromArgumentPositionOffset(array $arguments): ?int
+ {
+ foreach ($arguments as $i => $value) {
+ if ($value instanceof FromInterface) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Get/Get.php b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php
new file mode 100644
index 000000000..256676e9e
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php
@@ -0,0 +1,47 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (!is_array($arguments[static::$getArgumentPositionOffset])) {
+ throw new UnexpectedValueException('Wrong get argument type');
+ }
+
+ $patterns = [];
+
+ foreach ($arguments[static::$getArgumentPositionOffset] as $pattern) {
+ $patterns[] = self::$getModifier;
+ $patterns[] = $pattern;
+ }
+
+ $argumentsBeforeKeys = array_slice($arguments, 0, static::$getArgumentPositionOffset);
+ $argumentsAfterKeys = array_slice($arguments, static::$getArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBeforeKeys, $patterns, $argumentsAfterKeys));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php
new file mode 100644
index 000000000..3e0dfb133
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php
@@ -0,0 +1,54 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$indentArgumentPositionOffset] === '') {
+ array_splice($arguments, static::$indentArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$indentArgumentPositionOffset];
+
+ if (!is_string($argument)) {
+ throw new UnexpectedValueException('Indent argument value should be a string');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$indentArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$indentArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$indentModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php
new file mode 100644
index 000000000..7bab8205b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php
@@ -0,0 +1,54 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$newlineArgumentPositionOffset] === '') {
+ array_splice($arguments, static::$newlineArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$newlineArgumentPositionOffset];
+
+ if (!is_string($argument)) {
+ throw new UnexpectedValueException('Newline argument value should be a string');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$newlineArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$newlineArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$newlineModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php
new file mode 100644
index 000000000..39d3cb234
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php
@@ -0,0 +1,64 @@
+ 'NX',
+ 'xx' => 'XX',
+ ];
+
+ public function setArguments(array $arguments)
+ {
+ $argumentsLength = count($arguments);
+
+ if (static::$nxXxArgumentPositionOffset >= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (null === $arguments[static::$nxXxArgumentPositionOffset]) {
+ array_splice($arguments, static::$nxXxArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$nxXxArgumentPositionOffset];
+
+ if (!in_array(strtoupper($argument), self::$argumentEnum, true)) {
+ $enumValues = implode(', ', array_keys(self::$argumentEnum));
+ throw new UnexpectedValueException("Argument accepts only: {$enumValues} values");
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$nxXxArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$nxXxArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$argumentEnum[strtolower($argument)]],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Space.php b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php
new file mode 100644
index 000000000..5c99828f4
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php
@@ -0,0 +1,54 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$spaceArgumentPositionOffset] === '') {
+ array_splice($arguments, static::$spaceArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$spaceArgumentPositionOffset];
+
+ if (!is_string($argument)) {
+ throw new UnexpectedValueException('Space argument value should be a string');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$spaceArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$spaceArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$spaceModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Keys.php b/plugins/cache-redis/Predis/Command/Traits/Keys.php
new file mode 100644
index 000000000..5dc86ad7d
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Keys.php
@@ -0,0 +1,47 @@
+ $argumentsLength
+ || !is_array($arguments[static::$keysArgumentPositionOffset])
+ ) {
+ throw new UnexpectedValueException('Wrong keys argument type or position offset');
+ }
+
+ $keysArgument = $arguments[static::$keysArgumentPositionOffset];
+ $argumentsBeforeKeys = array_slice($arguments, 0, static::$keysArgumentPositionOffset);
+ $argumentsAfterKeys = array_slice($arguments, static::$keysArgumentPositionOffset + 1);
+
+ if ($withNumkeys) {
+ $numkeys = count($keysArgument);
+ parent::setArguments(array_merge($argumentsBeforeKeys, [$numkeys], $keysArgument, $argumentsAfterKeys));
+
+ return;
+ }
+
+ parent::setArguments(array_merge($argumentsBeforeKeys, $keysArgument, $argumentsAfterKeys));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/LeftRight.php b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php
new file mode 100644
index 000000000..181fbd143
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php
@@ -0,0 +1,60 @@
+ 'LEFT',
+ 'right' => 'RIGHT',
+ ];
+
+ public function setArguments(array $arguments)
+ {
+ $argumentsLength = count($arguments);
+
+ if (static::$leftRightArgumentPositionOffset >= $argumentsLength) {
+ $arguments[] = 'LEFT';
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$leftRightArgumentPositionOffset];
+
+ if (is_string($argument) && in_array(strtoupper($argument), self::$leftRightEnum, true)) {
+ $argument = self::$leftRightEnum[$argument];
+ } else {
+ $enumValues = implode(', ', array_keys(self::$leftRightEnum));
+ throw new UnexpectedValueException("Left/Right argument accepts only: {$enumValues} values");
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$leftRightArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$leftRightArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php
new file mode 100644
index 000000000..e24499472
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php
@@ -0,0 +1,54 @@
+= $argumentsLength
+ || false === $arguments[static::$limitArgumentPositionOffset]
+ ) {
+ parent::setArguments($argumentsBefore);
+
+ return;
+ }
+
+ $argument = $arguments[static::$limitArgumentPositionOffset];
+ $argumentsAfter = array_slice($arguments, static::$limitArgumentPositionOffset + 1);
+
+ if (true === $argument) {
+ parent::setArguments(array_merge($argumentsBefore, [self::$limitModifier], $argumentsAfter));
+
+ return;
+ }
+
+ if (!is_int($argument)) {
+ throw new UnexpectedValueException('Wrong limit argument type');
+ }
+
+ parent::setArguments(array_merge($argumentsBefore, [self::$limitModifier], [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php
new file mode 100644
index 000000000..3e47de9ab
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php
@@ -0,0 +1,50 @@
+getLimitArgumentPositionOffset($arguments);
+
+ if (null === $argumentPositionOffset) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $limitObject = $arguments[$argumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ $limitObject->toArray(),
+ $argumentsAfter
+ ));
+ }
+
+ private function getLimitArgumentPositionOffset(array $arguments): ?int
+ {
+ foreach ($arguments as $i => $value) {
+ if ($value instanceof LimitInterface) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php
new file mode 100644
index 000000000..f48f4c1e8
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php
@@ -0,0 +1,45 @@
+ 'MIN',
+ 'max' => 'MAX',
+ ];
+
+ public function resolveModifier(int $offset, array &$arguments): void
+ {
+ if ($offset >= count($arguments)) {
+ $arguments[$offset] = $this->modifierEnum['min'];
+
+ return;
+ }
+
+ if (!is_string($arguments[$offset]) || !array_key_exists($arguments[$offset], $this->modifierEnum)) {
+ throw new UnexpectedValueException('Wrong type of modifier given');
+ }
+
+ $arguments[$offset] = $this->modifierEnum[$arguments[$offset]];
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Replace.php b/plugins/cache-redis/Predis/Command/Traits/Replace.php
new file mode 100644
index 000000000..d193d66d3
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Replace.php
@@ -0,0 +1,34 @@
+ 'ASC',
+ 'desc' => 'DESC',
+ ];
+
+ public function setArguments(array $arguments)
+ {
+ $argumentsLength = count($arguments);
+
+ if (static::$sortArgumentPositionOffset >= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$sortArgumentPositionOffset];
+
+ if (null === $argument) {
+ array_splice($arguments, static::$sortArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (!in_array(strtoupper($argument), self::$sortingEnum, true)) {
+ $enumValues = implode(', ', array_keys(self::$sortingEnum));
+ throw new UnexpectedValueException("Sorting argument accepts only: {$enumValues} values");
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$sortArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$sortArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$sortingEnum[$argument]],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Storedist.php b/plugins/cache-redis/Predis/Command/Traits/Storedist.php
new file mode 100644
index 000000000..6feed1e8c
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Storedist.php
@@ -0,0 +1,49 @@
+= $argumentsLength
+ || false === $arguments[static::$storeDistArgumentPositionOffset]
+ ) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$storeDistArgumentPositionOffset];
+
+ if (true === $argument) {
+ $argument = 'STOREDIST';
+ } else {
+ throw new UnexpectedValueException('Wrong STOREDIST argument type');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$storeDistArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$storeDistArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Timeout.php b/plugins/cache-redis/Predis/Command/Traits/Timeout.php
new file mode 100644
index 000000000..fd33ea9cf
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Timeout.php
@@ -0,0 +1,53 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$timeoutArgumentPositionOffset] === -1) {
+ array_splice($arguments, static::$timeoutArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if ($arguments[static::$timeoutArgumentPositionOffset] < 1) {
+ throw new UnexpectedValueException('Wrong timeout argument value or position offset');
+ }
+
+ $argument = $arguments[static::$timeoutArgumentPositionOffset];
+ $argumentsBefore = array_slice($arguments, 0, static::$timeoutArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$timeoutArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$timeoutModifier],
+ [$argument],
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php
new file mode 100644
index 000000000..1ab13eca9
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php
@@ -0,0 +1,48 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ /** @var To|null $toArgument */
+ $toArgument = $arguments[static::$toArgumentPositionOffset];
+
+ if (null === $toArgument) {
+ array_splice($arguments, static::$toArgumentPositionOffset, 1, [false]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$toArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$toArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ $toArgument->toArray(),
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/Weights.php b/plugins/cache-redis/Predis/Command/Traits/Weights.php
new file mode 100644
index 000000000..1f175ed8b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/Weights.php
@@ -0,0 +1,61 @@
+= $argumentsLength) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ if (!is_array($arguments[static::$weightsArgumentPositionOffset])) {
+ throw new UnexpectedValueException('Wrong weights argument type');
+ }
+
+ $weightsArray = $arguments[static::$weightsArgumentPositionOffset];
+
+ if (empty($weightsArray)) {
+ unset($arguments[static::$weightsArgumentPositionOffset]);
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$weightsArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$weightsArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge(
+ $argumentsBefore,
+ [self::$weightsModifier],
+ $weightsArray,
+ $argumentsAfter
+ ));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php
new file mode 100644
index 000000000..797ae2e52
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php
@@ -0,0 +1,49 @@
+= $argumentsLength
+ || false === $arguments[static::$withCoordArgumentPositionOffset]
+ ) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$withCoordArgumentPositionOffset];
+
+ if (true === $argument) {
+ $argument = 'WITHCOORD';
+ } else {
+ throw new UnexpectedValueException('Wrong WITHCOORD argument type');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$withCoordArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$withCoordArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php
new file mode 100644
index 000000000..479606dca
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php
@@ -0,0 +1,45 @@
+= $argumentsLength
+ || false === $arguments[static::$withDistArgumentPositionOffset]
+ ) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$withDistArgumentPositionOffset];
+
+ if (true === $argument) {
+ $argument = 'WITHDIST';
+ } else {
+ throw new UnexpectedValueException('Wrong WITHDIST argument type');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$withDistArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$withDistArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php
new file mode 100644
index 000000000..c00f680b2
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php
@@ -0,0 +1,45 @@
+= $argumentsLength
+ || false === $arguments[static::$withHashArgumentPositionOffset]
+ ) {
+ parent::setArguments($arguments);
+
+ return;
+ }
+
+ $argument = $arguments[static::$withHashArgumentPositionOffset];
+
+ if (true === $argument) {
+ $argument = 'WITHHASH';
+ } else {
+ throw new UnexpectedValueException('Wrong WITHHASH argument type');
+ }
+
+ $argumentsBefore = array_slice($arguments, 0, static::$withHashArgumentPositionOffset);
+ $argumentsAfter = array_slice($arguments, static::$withHashArgumentPositionOffset + 1);
+
+ parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php
new file mode 100644
index 000000000..bc81d36c9
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php
@@ -0,0 +1,68 @@
+isWithScoreModifier()) {
+ $result = [];
+
+ for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) {
+ if (is_array($data[$i])) {
+ $result[$data[$i][0]] = $data[$i][1]; // Relay
+ } elseif (array_key_exists($i + 1, $data)) {
+ $result[$data[$i]] = $data[++$i];
+ }
+ }
+
+ return $result;
+ }
+
+ return $data;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php
new file mode 100644
index 000000000..4efb06584
--- /dev/null
+++ b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php
@@ -0,0 +1,34 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,12 +12,11 @@
namespace Predis;
+use Exception;
use Predis\Connection\NodeConnectionInterface;
/**
* Base exception class for network-related errors.
- *
- * @author Daniele Alessandri
*/
abstract class CommunicationException extends PredisException
{
@@ -26,15 +26,20 @@ abstract class CommunicationException extends PredisException
* @param NodeConnectionInterface $connection Connection that generated the exception.
* @param string $message Error message.
* @param int $code Error code.
- * @param \Exception $innerException Inner exception for wrapping the original error.
+ * @param Exception|null $innerException Inner exception for wrapping the original error.
*/
public function __construct(
NodeConnectionInterface $connection,
- $message = null,
- $code = null,
- \Exception $innerException = null
+ $message = '',
+ $code = 0,
+ Exception $innerException = null
) {
- parent::__construct($message, $code, $innerException);
+ parent::__construct(
+ is_null($message) ? '' : $message,
+ is_null($code) ? 0 : $code,
+ $innerException
+ );
+
$this->connection = $connection;
}
diff --git a/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php b/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php
new file mode 100644
index 000000000..262f8d64d
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php
@@ -0,0 +1,114 @@
+getConnectionInitializer($options, $value);
+ }
+
+ /**
+ * Wraps a user-supplied callable used to create a new aggregate connection.
+ *
+ * When the original callable acting as a connection initializer is executed
+ * by the client to create a new aggregate connection, it will receive the
+ * following arguments:
+ *
+ * - $parameters (same as passed to Predis\Client::__construct())
+ * - $options (options container, Predis\Configuration\OptionsInterface)
+ * - $option (current option, Predis\Configuration\OptionInterface)
+ *
+ * The original callable must return a valid aggregation connection instance
+ * of type Predis\Connection\AggregateConnectionInterface, this is enforced
+ * by the wrapper returned by this method and an exception is thrown when
+ * invalid values are returned.
+ *
+ * @param OptionsInterface $options Client options
+ * @param callable $callable Callable initializer
+ *
+ * @return callable
+ * @throws InvalidArgumentException
+ */
+ protected function getConnectionInitializer(OptionsInterface $options, callable $callable)
+ {
+ return function ($parameters = null, $autoaggregate = false) use ($callable, $options) {
+ $connection = call_user_func_array($callable, [&$parameters, $options, $this]);
+
+ if (!$connection instanceof AggregateConnectionInterface) {
+ throw new InvalidArgumentException(sprintf(
+ '%s expects the supplied callable to return an instance of %s, but %s was returned',
+ static::class,
+ AggregateConnectionInterface::class,
+ is_object($connection) ? get_class($connection) : gettype($connection)
+ ));
+ }
+
+ if ($parameters && $autoaggregate) {
+ static::aggregate($options, $connection, $parameters);
+ }
+
+ return $connection;
+ };
+ }
+
+ /**
+ * Adds single connections to an aggregate connection instance.
+ *
+ * @param OptionsInterface $options Client options
+ * @param AggregateConnectionInterface $connection Target aggregate connection
+ * @param array $nodes List of nodes to be added to the target aggregate connection
+ */
+ public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes)
+ {
+ $connections = $options->connections;
+
+ foreach ($nodes as $node) {
+ $connection->add($node instanceof NodeConnectionInterface ? $node : $connections->create($node));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Configuration/Option/CRC16.php b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php
new file mode 100644
index 000000000..b17144922
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php
@@ -0,0 +1,74 @@
+getHashGeneratorByDescription($options, $value);
+ } elseif ($value instanceof Hash\HashGeneratorInterface) {
+ return $value;
+ } else {
+ $class = get_class($this);
+ throw new InvalidArgumentException("$class expects a valid hash generator");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return function_exists('phpiredis_utils_crc16')
+ ? new Hash\PhpiredisCRC16()
+ : new Hash\CRC16();
+ }
+}
diff --git a/plugins/cache-redis/Predis/Configuration/Option/Cluster.php b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php
new file mode 100644
index 000000000..34b33de47
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php
@@ -0,0 +1,99 @@
+getConnectionInitializerByString($options, $value);
+ }
+
+ if (is_callable($value)) {
+ return $this->getConnectionInitializer($options, $value);
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ '%s expects either a string or a callable value, %s given',
+ static::class,
+ is_object($value) ? get_class($value) : gettype($value)
+ ));
+ }
+ }
+
+ /**
+ * Returns a connection initializer from a descriptive name.
+ *
+ * @param OptionsInterface $options Client options
+ * @param string $description Identifier of a replication backend (`predis`, `sentinel`)
+ *
+ * @return callable
+ */
+ protected function getConnectionInitializerByString(OptionsInterface $options, string $description)
+ {
+ switch ($description) {
+ case 'redis':
+ case 'redis-cluster':
+ return function ($parameters, $options, $option) {
+ return new RedisCluster($options->connections, new RedisStrategy($options->crc16));
+ };
+
+ case 'predis':
+ return $this->getDefaultConnectionInitializer();
+
+ default:
+ throw new InvalidArgumentException(sprintf(
+ '%s expects either `predis`, `redis` or `redis-cluster` as valid string values, `%s` given',
+ static::class,
+ $description
+ ));
+ }
+ }
+
+ /**
+ * Returns the default connection initializer.
+ *
+ * @return callable
+ */
+ protected function getDefaultConnectionInitializer()
+ {
+ return function ($parameters, $options, $option) {
+ return new PredisCluster();
+ };
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return $this->getConnectionInitializer(
+ $options,
+ $this->getDefaultConnectionInitializer()
+ );
+ }
+}
diff --git a/plugins/cache-redis/Predis/Configuration/Option/Commands.php b/plugins/cache-redis/Predis/Configuration/Option/Commands.php
new file mode 100644
index 000000000..2fbe00e74
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/Option/Commands.php
@@ -0,0 +1,146 @@
+createFactoryByArray($options, $value);
+ } elseif (is_string($value)) {
+ return $this->createFactoryByString($options, $value);
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ '%s expects a valid command factory',
+ static::class
+ ));
+ }
+ }
+
+ /**
+ * Creates a new default command factory from a named array.
+ *
+ * The factory instance is configured according to the supplied named array
+ * mapping command IDs (passed as keys) to the FCQN of classes implementing
+ * Predis\Command\CommandInterface.
+ *
+ * @param OptionsInterface $options Client options container
+ * @param array $value Named array mapping command IDs to classes
+ *
+ * @return FactoryInterface
+ */
+ protected function createFactoryByArray(OptionsInterface $options, array $value)
+ {
+ /**
+ * @var FactoryInterface
+ */
+ $commands = $this->getDefault($options);
+
+ foreach ($value as $commandID => $commandClass) {
+ if ($commandClass === null) {
+ $commands->undefine($commandID);
+ } else {
+ $commands->define($commandID, $commandClass);
+ }
+ }
+
+ return $commands;
+ }
+
+ /**
+ * Creates a new command factory from a descriptive string.
+ *
+ * The factory instance is configured according to the supplied descriptive
+ * string that identifies specific configurations of schemes and connection
+ * classes. Supported configuration values are:
+ *
+ * - "predis" returns the default command factory used by Predis
+ * - "raw" returns a command factory that creates only raw commands
+ * - "default" is simply an alias of "predis"
+ *
+ * @param OptionsInterface $options Client options container
+ * @param string $value Descriptive string identifying the desired configuration
+ *
+ * @return FactoryInterface
+ */
+ protected function createFactoryByString(OptionsInterface $options, string $value)
+ {
+ switch (strtolower($value)) {
+ case 'default':
+ case 'predis':
+ return $this->getDefault($options);
+
+ case 'raw':
+ return $this->createRawFactory($options);
+
+ default:
+ throw new InvalidArgumentException(sprintf(
+ '%s does not recognize `%s` as a supported configuration string',
+ static::class,
+ $value
+ ));
+ }
+ }
+
+ /**
+ * Creates a new raw command factory instance.
+ *
+ * @param OptionsInterface $options Client options container
+ */
+ protected function createRawFactory(OptionsInterface $options): FactoryInterface
+ {
+ $commands = new RawFactory();
+
+ if (isset($options->prefix)) {
+ throw new InvalidArgumentException(sprintf(
+ '%s does not support key prefixing', RawFactory::class
+ ));
+ }
+
+ return $commands;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ $commands = new RedisFactory();
+
+ if (isset($options->prefix)) {
+ $commands->setProcessor($options->prefix);
+ }
+
+ return $commands;
+ }
+}
diff --git a/plugins/cache-redis/Predis/Configuration/Option/Connections.php b/plugins/cache-redis/Predis/Configuration/Option/Connections.php
new file mode 100644
index 000000000..e37de4cad
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/Option/Connections.php
@@ -0,0 +1,152 @@
+createFactoryByArray($options, $value);
+ } elseif (is_string($value)) {
+ return $this->createFactoryByString($options, $value);
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ '%s expects a valid connection factory', static::class
+ ));
+ }
+ }
+
+ /**
+ * Creates a new connection factory from a named array.
+ *
+ * The factory instance is configured according to the supplied named array
+ * mapping URI schemes (passed as keys) to the FCQN of classes implementing
+ * Predis\Connection\NodeConnectionInterface, or callable objects acting as
+ * lazy initializers and returning new instances of classes implementing
+ * Predis\Connection\NodeConnectionInterface.
+ *
+ * @param OptionsInterface $options Client options
+ * @param array $value Named array mapping URI schemes to classes or callables
+ *
+ * @return FactoryInterface
+ */
+ protected function createFactoryByArray(OptionsInterface $options, array $value)
+ {
+ /**
+ * @var FactoryInterface
+ */
+ $factory = $this->getDefault($options);
+
+ foreach ($value as $scheme => $initializer) {
+ $factory->define($scheme, $initializer);
+ }
+
+ return $factory;
+ }
+
+ /**
+ * Creates a new connection factory from a descriptive string.
+ *
+ * The factory instance is configured according to the supplied descriptive
+ * string that identifies specific configurations of schemes and connection
+ * classes. Supported configuration values are:
+ *
+ * - "phpiredis-stream" maps tcp, redis, unix to PhpiredisStreamConnection
+ * - "phpiredis-socket" maps tcp, redis, unix to PhpiredisSocketConnection
+ * - "phpiredis" is an alias of "phpiredis-stream"
+ * - "relay" maps tcp, redis, unix, tls, rediss to RelayConnection
+ *
+ * @param OptionsInterface $options Client options
+ * @param string $value Descriptive string identifying the desired configuration
+ *
+ * @return FactoryInterface
+ */
+ protected function createFactoryByString(OptionsInterface $options, string $value)
+ {
+ /**
+ * @var FactoryInterface
+ */
+ $factory = $this->getDefault($options);
+
+ switch (strtolower($value)) {
+ case 'phpiredis':
+ case 'phpiredis-stream':
+ $factory->define('tcp', PhpiredisStreamConnection::class);
+ $factory->define('redis', PhpiredisStreamConnection::class);
+ $factory->define('unix', PhpiredisStreamConnection::class);
+ break;
+
+ case 'phpiredis-socket':
+ $factory->define('tcp', PhpiredisSocketConnection::class);
+ $factory->define('redis', PhpiredisSocketConnection::class);
+ $factory->define('unix', PhpiredisSocketConnection::class);
+ break;
+
+ case 'relay':
+ $factory->define('tcp', RelayConnection::class);
+ $factory->define('redis', RelayConnection::class);
+ $factory->define('unix', RelayConnection::class);
+ break;
+
+ case 'default':
+ return $factory;
+
+ default:
+ throw new InvalidArgumentException(sprintf(
+ '%s does not recognize `%s` as a supported configuration string', static::class, $value
+ ));
+ }
+
+ return $factory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ $factory = new Factory();
+
+ if ($options->defined('parameters')) {
+ $factory->setDefaultParameters($options->parameters);
+ }
+
+ return $factory;
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/ExceptionsOption.php b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php
similarity index 73%
rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/ExceptionsOption.php
rename to plugins/cache-redis/Predis/Configuration/Option/Exceptions.php
index 337733e4b..6834272f0 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/ExceptionsOption.php
+++ b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php
@@ -3,21 +3,23 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
+
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
/**
* Configures whether consumers (such as the client) should throw exceptions on
* Redis errors (-ERR responses) or just return instances of error responses.
- *
- * @author Daniele Alessandri
*/
-class ExceptionsOption implements OptionInterface
+class Exceptions implements OptionInterface
{
/**
* {@inheritdoc}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/PrefixOption.php b/plugins/cache-redis/Predis/Configuration/Option/Prefix.php
similarity index 66%
rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/PrefixOption.php
rename to plugins/cache-redis/Predis/Configuration/Option/Prefix.php
index 5827cdc37..772454b36 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/PrefixOption.php
+++ b/plugins/cache-redis/Predis/Configuration/Option/Prefix.php
@@ -3,35 +3,40 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Configuration;
+namespace Predis\Configuration\Option;
use Predis\Command\Processor\KeyPrefixProcessor;
use Predis\Command\Processor\ProcessorInterface;
+use Predis\Configuration\OptionInterface;
+use Predis\Configuration\OptionsInterface;
/**
* Configures a command processor that apply the specified prefix string to a
* series of Redis commands considered prefixable.
- *
- * @author Daniele Alessandri
*/
-class PrefixOption implements OptionInterface
+class Prefix implements OptionInterface
{
/**
* {@inheritdoc}
*/
public function filter(OptionsInterface $options, $value)
{
+ if (is_callable($value)) {
+ $value = call_user_func($value, $options);
+ }
+
if ($value instanceof ProcessorInterface) {
return $value;
}
- return new KeyPrefixProcessor($value);
+ return new KeyPrefixProcessor((string) $value);
}
/**
diff --git a/plugins/cache-redis/Predis/Configuration/Option/Replication.php b/plugins/cache-redis/Predis/Configuration/Option/Replication.php
new file mode 100644
index 000000000..b0f132d19
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/Option/Replication.php
@@ -0,0 +1,126 @@
+getConnectionInitializerByString($options, $value);
+ }
+
+ if (is_callable($value)) {
+ return $this->getConnectionInitializer($options, $value);
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ '%s expects either a string or a callable value, %s given',
+ static::class,
+ is_object($value) ? get_class($value) : gettype($value)
+ ));
+ }
+ }
+
+ /**
+ * Returns a connection initializer (callable) from a descriptive string.
+ *
+ * Each connection initializer is specialized for the specified replication
+ * backend so that all the necessary steps for the configuration of the new
+ * aggregate connection are performed inside the initializer and the client
+ * receives a ready-to-use connection.
+ *
+ * Supported configuration values are:
+ *
+ * - `predis` for unmanaged replication setups
+ * - `redis-sentinel` for replication setups managed by redis-sentinel
+ * - `sentinel` is an alias of `redis-sentinel`
+ *
+ * @param OptionsInterface $options Client options
+ * @param string $description Identifier of a replication backend
+ *
+ * @return callable
+ */
+ protected function getConnectionInitializerByString(OptionsInterface $options, string $description)
+ {
+ switch ($description) {
+ case 'sentinel':
+ case 'redis-sentinel':
+ return function ($parameters, $options) {
+ return new SentinelReplication($options->service, $parameters, $options->connections);
+ };
+
+ case 'predis':
+ return $this->getDefaultConnectionInitializer();
+
+ default:
+ throw new InvalidArgumentException(sprintf(
+ '%s expects either `predis`, `sentinel` or `redis-sentinel` as valid string values, `%s` given',
+ static::class,
+ $description
+ ));
+ }
+ }
+
+ /**
+ * Returns the default connection initializer.
+ *
+ * @return callable
+ */
+ protected function getDefaultConnectionInitializer()
+ {
+ return function ($parameters, $options) {
+ $connection = new MasterSlaveReplication();
+
+ if ($options->autodiscovery) {
+ $connection->setConnectionFactory($options->connections);
+ $connection->setAutoDiscovery(true);
+ }
+
+ return $connection;
+ };
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes)
+ {
+ if (!$connection instanceof SentinelReplication) {
+ parent::aggregate($options, $connection, $nodes);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefault(OptionsInterface $options)
+ {
+ return $this->getConnectionInitializer(
+ $options,
+ $this->getDefaultConnectionInitializer()
+ );
+ }
+}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php b/plugins/cache-redis/Predis/Configuration/OptionInterface.php
similarity index 89%
rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php
rename to plugins/cache-redis/Predis/Configuration/OptionInterface.php
index b31e0c98f..538fc0ba7 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php
+++ b/plugins/cache-redis/Predis/Configuration/OptionInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -14,8 +15,6 @@ namespace Predis\Configuration;
/**
* Defines an handler used by Predis\Configuration\Options to filter, validate
* or return default values for a given option.
- *
- * @author Daniele Alessandri
*/
interface OptionInterface
{
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/Options.php b/plugins/cache-redis/Predis/Configuration/Options.php
similarity index 53%
rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/Options.php
rename to plugins/cache-redis/Predis/Configuration/Options.php
index 6f3b331b5..3fff04129 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/Options.php
+++ b/plugins/cache-redis/Predis/Configuration/Options.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -12,44 +13,39 @@
namespace Predis\Configuration;
/**
- * Manages Predis options with filtering, conversion and lazy initialization of
- * values using a mini-DI container approach.
+ * Default client options container for Predis\Client.
+ *
+ * Pre-defined options have their specialized handlers that can filter, convert
+ * an lazily initialize values in a mini-DI container approach.
*
* {@inheritdoc}
- *
- * @author Daniele Alessandri
*/
class Options implements OptionsInterface
{
+ /** @var array */
+ protected $handlers = [
+ 'aggregate' => Option\Aggregate::class,
+ 'cluster' => Option\Cluster::class,
+ 'replication' => Option\Replication::class,
+ 'connections' => Option\Connections::class,
+ 'commands' => Option\Commands::class,
+ 'exceptions' => Option\Exceptions::class,
+ 'prefix' => Option\Prefix::class,
+ 'crc16' => Option\CRC16::class,
+ ];
+
+ /** @var array */
+ protected $options = [];
+
+ /** @var array */
protected $input;
- protected $options;
- protected $handlers;
/**
- * @param array $options Array of options with their values
+ * @param array $options Named array of client options
*/
- public function __construct(array $options = array())
+ public function __construct(array $options = null)
{
- $this->input = $options;
- $this->options = array();
- $this->handlers = $this->getHandlers();
- }
-
- /**
- * Ensures that the default options are initialized.
- *
- * @return array
- */
- protected function getHandlers()
- {
- return array(
- 'cluster' => 'Predis\Configuration\ClusterOption',
- 'connections' => 'Predis\Configuration\ConnectionFactoryOption',
- 'exceptions' => 'Predis\Configuration\ExceptionsOption',
- 'prefix' => 'Predis\Configuration\PrefixOption',
- 'profile' => 'Predis\Configuration\ProfileOption',
- 'replication' => 'Predis\Configuration\ReplicationOption',
- );
+ $this->input = $options ?? [];
}
/**
@@ -70,10 +66,10 @@ class Options implements OptionsInterface
*/
public function defined($option)
{
- return (
- array_key_exists($option, $this->options) ||
- array_key_exists($option, $this->input)
- );
+ return
+ array_key_exists($option, $this->options)
+ || array_key_exists($option, $this->input)
+ ;
}
/**
@@ -82,8 +78,8 @@ class Options implements OptionsInterface
public function __isset($option)
{
return (
- array_key_exists($option, $this->options) ||
- array_key_exists($option, $this->input)
+ array_key_exists($option, $this->options)
+ || array_key_exists($option, $this->input)
) && $this->__get($option) !== null;
}
@@ -100,14 +96,12 @@ class Options implements OptionsInterface
$value = $this->input[$option];
unset($this->input[$option]);
- if (is_object($value) && method_exists($value, '__invoke')) {
- $value = $value($this, $option);
- }
-
if (isset($this->handlers[$option])) {
$handler = $this->handlers[$option];
$handler = new $handler();
$value = $handler->filter($this, $value);
+ } elseif (is_object($value) && method_exists($value, '__invoke')) {
+ $value = $value($this);
}
return $this->options[$option] = $value;
diff --git a/plugins/cache-redis/Predis/Configuration/OptionsInterface.php b/plugins/cache-redis/Predis/Configuration/OptionsInterface.php
new file mode 100644
index 000000000..597a0579b
--- /dev/null
+++ b/plugins/cache-redis/Predis/Configuration/OptionsInterface.php
@@ -0,0 +1,63 @@
+
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -11,15 +12,15 @@
namespace Predis\Connection;
+use InvalidArgumentException;
use Predis\Command\CommandInterface;
+use Predis\Command\RawCommand;
use Predis\CommunicationException;
use Predis\Protocol\ProtocolException;
/**
* Base class with the common logic used by connection classes to communicate
* with Redis.
- *
- * @author Daniele Alessandri
*/
abstract class AbstractConnection implements NodeConnectionInterface
{
@@ -27,7 +28,11 @@ abstract class AbstractConnection implements NodeConnectionInterface
private $cachedId;
protected $parameters;
- protected $initCommands = array();
+
+ /**
+ * @var RawCommand[]
+ */
+ protected $initCommands = [];
/**
* @param ParametersInterface $parameters Initialization parameters for the connection.
@@ -51,24 +56,10 @@ abstract class AbstractConnection implements NodeConnectionInterface
*
* @param ParametersInterface $parameters Initialization parameters for the connection.
*
- * @throws \InvalidArgumentException
- *
* @return ParametersInterface
+ * @throws InvalidArgumentException
*/
- protected function assertParameters(ParametersInterface $parameters)
- {
- switch ($parameters->scheme) {
- case 'tcp':
- case 'redis':
- case 'unix':
- break;
-
- default:
- throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'.");
- }
-
- return $parameters;
- }
+ abstract protected function assertParameters(ParametersInterface $parameters);
/**
* Creates the underlying resource used to communicate with Redis.
@@ -115,6 +106,14 @@ abstract class AbstractConnection implements NodeConnectionInterface
$this->initCommands[] = $command;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getInitCommands(): array
+ {
+ return $this->initCommands;
+ }
+
/**
* {@inheritdoc}
*/
@@ -133,39 +132,16 @@ abstract class AbstractConnection implements NodeConnectionInterface
return $this->read();
}
- /**
- * Helper method that returns an exception message augmented with useful
- * details from the connection parameters.
- *
- * @param string $message Error message.
- *
- * @return string
- */
- private function createExceptionMessage($message)
- {
- $parameters = $this->parameters;
-
- if ($parameters->scheme === 'unix') {
- return "$message [$parameters->scheme:$parameters->path]";
- }
-
- if (filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
- return "$message [$parameters->scheme://[$parameters->host]:$parameters->port]";
- }
-
- return "$message [$parameters->scheme://$parameters->host:$parameters->port]";
- }
-
/**
* Helper method to handle connection errors.
*
* @param string $message Error message.
* @param int $code Error code.
*/
- protected function onConnectionError($message, $code = null)
+ protected function onConnectionError($message, $code = 0)
{
CommunicationException::handle(
- new ConnectionException($this, static::createExceptionMessage($message), $code)
+ new ConnectionException($this, "$message [{$this->getParameters()}]", $code)
);
}
@@ -177,7 +153,7 @@ abstract class AbstractConnection implements NodeConnectionInterface
protected function onProtocolError($message)
{
CommunicationException::handle(
- new ProtocolException($this, static::createExceptionMessage($message))
+ new ProtocolException($this, "$message [{$this->getParameters()}]")
);
}
@@ -234,6 +210,6 @@ abstract class AbstractConnection implements NodeConnectionInterface
*/
public function __sleep()
{
- return array('parameters', 'initCommands');
+ return ['parameters', 'initCommands'];
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php b/plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php
similarity index 89%
rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php
rename to plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php
index 7eeaede76..8864bba53 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php
+++ b/plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php
@@ -3,7 +3,8 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -16,8 +17,6 @@ use Predis\Command\CommandInterface;
/**
* Defines a virtual connection composed of multiple connection instances to
* single Redis nodes.
- *
- * @author Daniele Alessandri
*/
interface AggregateConnectionInterface extends ConnectionInterface
{
@@ -44,7 +43,7 @@ interface AggregateConnectionInterface extends ConnectionInterface
*
* @return NodeConnectionInterface
*/
- public function getConnection(CommandInterface $command);
+ public function getConnectionByCommand(CommandInterface $command);
/**
* Returns a connection instance from the aggregate connection by its alias.
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php b/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php
similarity index 75%
rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php
rename to plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php
index af0f5aab5..79c6f96e9 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php
+++ b/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php
@@ -3,21 +3,20 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Connection\Aggregate;
+namespace Predis\Connection\Cluster;
use Predis\Connection\AggregateConnectionInterface;
/**
* Defines a cluster of Redis servers formed by aggregating multiple connection
* instances to single Redis nodes.
- *
- * @author Daniele Alessandri
*/
interface ClusterInterface extends AggregateConnectionInterface
{
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/PredisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php
similarity index 62%
rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/PredisCluster.php
rename to plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php
index 33f98bf2e..c30fd09d3 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/PredisCluster.php
+++ b/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php
@@ -3,32 +3,50 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Connection\Aggregate;
+namespace Predis\Connection\Cluster;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
use Predis\Cluster\PredisStrategy;
use Predis\Cluster\StrategyInterface;
use Predis\Command\CommandInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\NotSupportedException;
+use ReturnTypeWillChange;
+use Traversable;
/**
* Abstraction for a cluster of aggregate connections to various Redis servers
* implementing client-side sharding based on pluggable distribution strategies.
- *
- * @author Daniele Alessandri
- *
- * @todo Add the ability to remove connections from pool.
*/
-class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
+class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
{
- private $pool;
+ /**
+ * @var NodeConnectionInterface[]
+ */
+ private $pool = [];
+
+ /**
+ * @var NodeConnectionInterface[]
+ */
+ private $aliases = [];
+
+ /**
+ * @var StrategyInterface
+ */
private $strategy;
+
+ /**
+ * @var \Predis\Cluster\Distributor\DistributorInterface
+ */
private $distributor;
/**
@@ -36,7 +54,6 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function __construct(StrategyInterface $strategy = null)
{
- $this->pool = array();
$this->strategy = $strategy ?: new PredisStrategy();
$this->distributor = $this->strategy->getDistributor();
}
@@ -82,14 +99,13 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
{
$parameters = $connection->getParameters();
+ $this->pool[(string) $connection] = $connection;
+
if (isset($parameters->alias)) {
- $this->pool[$parameters->alias] = $connection;
- } else {
- $this->pool[] = $connection;
+ $this->aliases[$parameters->alias] = $connection;
}
- $weight = isset($parameters->weight) ? $parameters->weight : null;
- $this->distributor->add($connection, $weight);
+ $this->distributor->add($connection, $parameters->weight);
}
/**
@@ -97,36 +113,24 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function remove(NodeConnectionInterface $connection)
{
- if (($id = array_search($connection, $this->pool, true)) !== false) {
+ if (false !== $id = array_search($connection, $this->pool, true)) {
unset($this->pool[$id]);
$this->distributor->remove($connection);
+ if ($this->aliases && $alias = $connection->getParameters()->alias) {
+ unset($this->aliases[$alias]);
+ }
+
return true;
}
return false;
}
- /**
- * Removes a connection instance using its alias or index.
- *
- * @param string $connectionID Alias or index of a connection.
- *
- * @return bool Returns true if the connection was in the pool.
- */
- public function removeById($connectionID)
- {
- if ($connection = $this->getConnectionById($connectionID)) {
- return $this->remove($connection);
- }
-
- return false;
- }
-
/**
* {@inheritdoc}
*/
- public function getConnection(CommandInterface $command)
+ public function getConnectionByCommand(CommandInterface $command)
{
$slot = $this->strategy->getSlot($command);
@@ -136,17 +140,39 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
);
}
- $node = $this->distributor->getBySlot($slot);
-
- return $node;
+ return $this->distributor->getBySlot($slot);
}
/**
* {@inheritdoc}
*/
- public function getConnectionById($connectionID)
+ public function getConnectionById($id)
{
- return isset($this->pool[$connectionID]) ? $this->pool[$connectionID] : null;
+ return $this->pool[$id] ?? null;
+ }
+
+ /**
+ * Returns a connection instance by its alias.
+ *
+ * @param string $alias Connection alias.
+ *
+ * @return NodeConnectionInterface|null
+ */
+ public function getConnectionByAlias($alias)
+ {
+ return $this->aliases[$alias] ?? null;
+ }
+
+ /**
+ * Retrieves a connection instance by slot.
+ *
+ * @param string $slot Slot name.
+ *
+ * @return NodeConnectionInterface|null
+ */
+ public function getConnectionBySlot($slot)
+ {
+ return $this->distributor->getBySlot($slot);
}
/**
@@ -159,9 +185,8 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
public function getConnectionByKey($key)
{
$hash = $this->strategy->getSlotByKey($key);
- $node = $this->distributor->getBySlot($hash);
- return $node;
+ return $this->distributor->getBySlot($hash);
}
/**
@@ -176,19 +201,21 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
}
/**
- * {@inheritdoc}
+ * @return int
*/
+ #[ReturnTypeWillChange]
public function count()
{
return count($this->pool);
}
/**
- * {@inheritdoc}
+ * @return Traversable
*/
+ #[ReturnTypeWillChange]
public function getIterator()
{
- return new \ArrayIterator($this->pool);
+ return new ArrayIterator($this->pool);
}
/**
@@ -196,7 +223,7 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function writeRequest(CommandInterface $command)
{
- $this->getConnection($command)->writeRequest($command);
+ $this->getConnectionByCommand($command)->writeRequest($command);
}
/**
@@ -204,7 +231,7 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function readResponse(CommandInterface $command)
{
- return $this->getConnection($command)->readResponse($command);
+ return $this->getConnectionByCommand($command)->readResponse($command);
}
/**
@@ -212,24 +239,6 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function executeCommand(CommandInterface $command)
{
- return $this->getConnection($command)->executeCommand($command);
- }
-
- /**
- * Executes the specified Redis command on all the nodes of a cluster.
- *
- * @param CommandInterface $command A Redis command.
- *
- * @return array
- */
- public function executeCommandOnNodes(CommandInterface $command)
- {
- $responses = array();
-
- foreach ($this->pool as $connection) {
- $responses[] = $connection->executeCommand($command);
- }
-
- return $responses;
+ return $this->getConnectionByCommand($command)->executeCommand($command);
}
}
diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/RedisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php
similarity index 59%
rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/RedisCluster.php
rename to plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php
index 337c28702..7f3013c17 100644
--- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/RedisCluster.php
+++ b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php
@@ -3,22 +3,35 @@
/*
* This file is part of the Predis package.
*
- * (c) Daniele Alessandri
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Predis\Connection\Aggregate;
+namespace Predis\Connection\Cluster;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use OutOfBoundsException;
+use Predis\ClientException;
use Predis\Cluster\RedisStrategy as RedisClusterStrategy;
+use Predis\Cluster\SlotMap;
use Predis\Cluster\StrategyInterface;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
+use Predis\Connection\ConnectionException;
use Predis\Connection\FactoryInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\NotSupportedException;
+use Predis\Response\Error as ErrorResponse;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
+use Predis\Response\ServerException;
+use ReturnTypeWillChange;
+use Throwable;
+use Traversable;
/**
* Abstraction for a Redis-backed cluster of nodes (Redis >= 3.0.0).
@@ -39,18 +52,17 @@ use Predis\Response\ErrorInterface as ErrorResponseInterface;
* of the nodes and optionally enable such a behaviour upon -MOVED redirections.
* Asking for the cluster configuration to Redis is actually done by issuing a
* CLUSTER SLOTS command to a random node in the pool.
- *
- * @author Daniele Alessandri
*/
-class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
+class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
{
private $useClusterSlots = true;
- private $defaultParameters = array();
- private $pool = array();
- private $slots = array();
- private $slotsMap;
+ private $pool = [];
+ private $slots = [];
+ private $slotmap;
private $strategy;
private $connections;
+ private $retryLimit = 5;
+ private $retryInterval = 10;
/**
* @param FactoryInterface $connections Optional connection factory.
@@ -62,6 +74,41 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
) {
$this->connections = $connections;
$this->strategy = $strategy ?: new RedisClusterStrategy();
+ $this->slotmap = new SlotMap();
+ }
+
+ /**
+ * Sets the maximum number of retries for commands upon server failure.
+ *
+ * -1 = unlimited retry attempts
+ * 0 = no retry attempts (fails immediately)
+ * n = fail only after n retry attempts
+ *
+ * @param int $retry Number of retry attempts.
+ */
+ public function setRetryLimit($retry)
+ {
+ $this->retryLimit = (int) $retry;
+ }
+
+ /**
+ * Sets the initial retry interval (milliseconds).
+ *
+ * @param int $retryInterval Milliseconds between retries.
+ */
+ public function setRetryInterval($retryInterval)
+ {
+ $this->retryInterval = (int) $retryInterval;
+ }
+
+ /**
+ * Returns the retry interval (milliseconds).
+ *
+ * @return int Milliseconds between retries.
+ */
+ public function getRetryInterval()
+ {
+ return (int) $this->retryInterval;
}
/**
@@ -104,7 +151,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
public function add(NodeConnectionInterface $connection)
{
$this->pool[(string) $connection] = $connection;
- unset($this->slotsMap);
+ $this->slotmap->reset();
}
/**
@@ -113,10 +160,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
public function remove(NodeConnectionInterface $connection)
{
if (false !== $id = array_search($connection, $this->pool, true)) {
- unset(
- $this->pool[$id],
- $this->slotsMap
- );
+ $this->slotmap->reset();
+ $this->slots = array_diff($this->slots, [$connection]);
+ unset($this->pool[$id]);
return true;
}
@@ -134,10 +180,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
public function removeById($connectionID)
{
if (isset($this->pool[$connectionID])) {
- unset(
- $this->pool[$connectionID],
- $this->slotsMap
- );
+ $this->slotmap->reset();
+ $this->slots = array_diff($this->slots, [$connectionID]);
+ unset($this->pool[$connectionID]);
return true;
}
@@ -154,9 +199,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
* initialization have the "slots" parameter properly set accordingly to the
* current cluster configuration.
*/
- public function buildSlotsMap()
+ public function buildSlotMap()
{
- $this->slotsMap = array();
+ $this->slotmap->reset();
foreach ($this->pool as $connectionID => $connection) {
$parameters = $connection->getParameters();
@@ -165,80 +210,90 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
continue;
}
- $slots = explode('-', $parameters->slots, 2);
- $this->setSlots($slots[0], $slots[1], $connectionID);
+ foreach (explode(',', $parameters->slots) as $slotRange) {
+ $slots = explode('-', $slotRange, 2);
+
+ if (!isset($slots[1])) {
+ $slots[1] = $slots[0];
+ }
+
+ $this->slotmap->setSlots($slots[0], $slots[1], $connectionID);
+ }
}
}
+ /**
+ * Queries the specified node of the cluster to fetch the updated slots map.
+ *
+ * When the connection fails, this method tries to execute the same command
+ * on a different connection picked at random from the pool of known nodes,
+ * up until the retry limit is reached.
+ *
+ * @param NodeConnectionInterface $connection Connection to a node of the cluster.
+ *
+ * @return mixed
+ */
+ private function queryClusterNodeForSlotMap(NodeConnectionInterface $connection)
+ {
+ $retries = 0;
+ $retryAfter = $this->retryInterval;
+ $command = RawCommand::create('CLUSTER', 'SLOTS');
+
+ while ($retries <= $this->retryLimit) {
+ try {
+ $response = $connection->executeCommand($command);
+ break;
+ } catch (ConnectionException $exception) {
+ $connection = $exception->getConnection();
+ $connection->disconnect();
+
+ $this->remove($connection);
+
+ if ($retries === $this->retryLimit) {
+ throw $exception;
+ }
+
+ if (!$connection = $this->getRandomConnection()) {
+ throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`');
+ }
+
+ usleep($retryAfter * 1000);
+ $retryAfter = $retryAfter * 2;
+ ++$retries;
+ }
+ }
+
+ return $response;
+ }
+
/**
* Generates an updated slots map fetching the cluster configuration using
* the CLUSTER SLOTS command against the specified node or a random one from
* the pool.
*
* @param NodeConnectionInterface $connection Optional connection instance.
- *
- * @return array
*/
- public function askSlotsMap(NodeConnectionInterface $connection = null)
+ public function askSlotMap(NodeConnectionInterface $connection = null)
{
if (!$connection && !$connection = $this->getRandomConnection()) {
- return array();
+ return;
}
- $command = RawCommand::create('CLUSTER', 'SLOTS');
- $response = $connection->executeCommand($command);
+ $this->slotmap->reset();
+
+ $response = $this->queryClusterNodeForSlotMap($connection);
foreach ($response as $slots) {
// We only support master servers for now, so we ignore subsequent
// elements in the $slots array identifying slaves.
- list($start, $end, $master) = $slots;
+ [$start, $end, $master] = $slots;
if ($master[0] === '') {
- $this->setSlots($start, $end, (string) $connection);
+ $this->slotmap->setSlots($start, $end, (string) $connection);
} else {
- $this->setSlots($start, $end, "{$master[0]}:{$master[1]}");
+ $this->slotmap->setSlots($start, $end, "{$master[0]}:{$master[1]}");
}
}
-
- return $this->slotsMap;
- }
-
- /**
- * Returns the current slots map for the cluster.
- *
- * @return array
- */
- public function getSlotsMap()
- {
- if (!isset($this->slotsMap)) {
- $this->slotsMap = array();
- }
-
- return $this->slotsMap;
- }
-
- /**
- * Pre-associates a connection to a slots range to avoid runtime guessing.
- *
- * @param int $first Initial slot of the range.
- * @param int $last Last slot of the range.
- * @param NodeConnectionInterface|string $connection ID or connection instance.
- *
- * @throws \OutOfBoundsException
- */
- public function setSlots($first, $last, $connection)
- {
- if ($first < 0x0000 || $first > 0x3FFF ||
- $last < 0x0000 || $last > 0x3FFF ||
- $last < $first
- ) {
- throw new \OutOfBoundsException(
- "Invalid slot range for $connection: [$first-$last]."
- );
- }
-
- $slots = array_fill($first, $last - $first + 1, (string) $connection);
- $this->slotsMap = $this->getSlotsMap() + $slots;
}
/**
@@ -252,12 +307,16 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
protected function guessNode($slot)
{
- if (!isset($this->slotsMap)) {
- $this->buildSlotsMap();
+ if (!$this->pool) {
+ throw new ClientException('No connections available in the pool');
}
- if (isset($this->slotsMap[$slot])) {
- return $this->slotsMap[$slot];
+ if ($this->slotmap->isEmpty()) {
+ $this->buildSlotMap();
+ }
+
+ if ($node = $this->slotmap[$slot]) {
+ return $node;
}
$count = count($this->pool);
@@ -278,20 +337,16 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
{
$separator = strrpos($connectionID, ':');
- $parameters = array_merge($this->defaultParameters, array(
+ return $this->connections->create([
'host' => substr($connectionID, 0, $separator),
'port' => substr($connectionID, $separator + 1),
- ));
-
- $connection = $this->connections->create($parameters);
-
- return $connection;
+ ]);
}
/**
* {@inheritdoc}
*/
- public function getConnection(CommandInterface $command)
+ public function getConnectionByCommand(CommandInterface $command)
{
$slot = $this->strategy->getSlot($command);
@@ -313,14 +368,13 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*
* @param int $slot Slot index.
*
- * @throws \OutOfBoundsException
- *
* @return NodeConnectionInterface
+ * @throws OutOfBoundsException
*/
public function getConnectionBySlot($slot)
{
- if ($slot < 0x0000 || $slot > 0x3FFF) {
- throw new \OutOfBoundsException("Invalid slot [$slot].");
+ if (!SlotMap::isValid($slot)) {
+ throw new OutOfBoundsException("Invalid slot [$slot].");
}
if (isset($this->slots[$slot])) {
@@ -342,9 +396,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function getConnectionById($connectionID)
{
- if (isset($this->pool[$connectionID])) {
- return $this->pool[$connectionID];
- }
+ return $this->pool[$connectionID] ?? null;
}
/**
@@ -354,9 +406,11 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
protected function getRandomConnection()
{
- if ($this->pool) {
- return $this->pool[array_rand($this->pool)];
+ if (!$this->pool) {
+ return null;
}
+
+ return $this->pool[array_rand($this->pool)];
}
/**
@@ -370,6 +424,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
{
$this->pool[(string) $connection] = $connection;
$this->slots[(int) $slot] = $connection;
+ $this->slotmap[(int) $slot] = $connection;
}
/**
@@ -407,20 +462,19 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
protected function onMovedResponse(CommandInterface $command, $details)
{
- list($slot, $connectionID) = explode(' ', $details, 2);
+ [$slot, $connectionID] = explode(' ', $details, 2);
if (!$connection = $this->getConnectionById($connectionID)) {
$connection = $this->createConnection($connectionID);
}
if ($this->useClusterSlots) {
- $this->askSlotsMap($connection);
+ $this->askSlotMap($connection);
}
$this->move($connection, $slot);
- $response = $this->executeCommand($command);
- return $response;
+ return $this->executeCommand($command);
}
/**
@@ -434,14 +488,73 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
protected function onAskResponse(CommandInterface $command, $details)
{
- list($slot, $connectionID) = explode(' ', $details, 2);
+ [$slot, $connectionID] = explode(' ', $details, 2);
if (!$connection = $this->getConnectionById($connectionID)) {
$connection = $this->createConnection($connectionID);
}
$connection->executeCommand(RawCommand::create('ASKING'));
- $response = $connection->executeCommand($command);
+
+ return $connection->executeCommand($command);
+ }
+
+ /**
+ * Ensures that a command is executed one more time on connection failure.
+ *
+ * The connection to the node that generated the error is evicted from the
+ * pool before trying to fetch an updated slots map from another node. If
+ * the new slots map points to an unreachable server the client gives up and
+ * throws the exception as the nodes participating in the cluster may still
+ * have to agree that something changed in the configuration of the cluster.
+ *
+ * @param CommandInterface $command Command instance.
+ * @param string $method Actual method.
+ *
+ * @return mixed
+ */
+ private function retryCommandOnFailure(CommandInterface $command, $method)
+ {
+ $retries = 0;
+ $retryAfter = $this->retryInterval;
+
+ while ($retries <= $this->retryLimit) {
+ try {
+ $response = $this->getConnectionByCommand($command)->$method($command);
+
+ if ($response instanceof ErrorResponse) {
+ $message = $response->getMessage();
+
+ if (strpos($message, 'CLUSTERDOWN') !== false) {
+ throw new ServerException($message);
+ }
+ }
+
+ break;
+ } catch (Throwable $exception) {
+ usleep($retryAfter * 1000);
+ $retryAfter = $retryAfter * 2;
+
+ if ($exception instanceof ConnectionException) {
+ $connection = $exception->getConnection();
+
+ if ($connection) {
+ $connection->disconnect();
+ $this->remove($connection);
+ }
+ }
+
+ if ($retries === $this->retryLimit) {
+ throw $exception;
+ }
+
+ if ($this->useClusterSlots) {
+ $this->askSlotMap();
+ }
+
+ ++$retries;
+ }
+ }
return $response;
}
@@ -451,7 +564,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function writeRequest(CommandInterface $command)
{
- $this->getConnection($command)->writeRequest($command);
+ $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
@@ -459,7 +572,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function readResponse(CommandInterface $command)
{
- return $this->getConnection($command)->readResponse($command);
+ return $this->retryCommandOnFailure($command, __FUNCTION__);
}
/**
@@ -467,8 +580,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
*/
public function executeCommand(CommandInterface $command)
{
- $connection = $this->getConnection($command);
- $response = $connection->executeCommand($command);
+ $response = $this->retryCommandOnFailure($command, __FUNCTION__);
if ($response instanceof ErrorResponseInterface) {
return $this->onErrorResponse($command, $response);
@@ -478,19 +590,45 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable
}
/**
- * {@inheritdoc}
+ * @return int
*/
+ #[ReturnTypeWillChange]
public function count()
{
return count($this->pool);
}
/**
- * {@inheritdoc}
+ * @return Traversable