Merge branch 'master' into php81

This commit is contained in:
the-djmaze 2026-10-23 13:56:32 +02:00
commit 51de013007
1312 changed files with 58311 additions and 21890 deletions

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM alpine:3.18.5 AS builder FROM alpine:3.24.3 AS builder
RUN apk add --no-cache php82 php82-json php-phar php-zip RUN apk add --no-cache php85 php85-json php-phar php-zip
RUN apk add --no-cache npm RUN apk add --no-cache npm
RUN npm install -g gulp yarn RUN npm install -g gulp yarn
WORKDIR /source WORKDIR /source
@ -23,7 +23,7 @@ RUN set -eux; \
rm -v /snappymail/README.md /snappymail/_include.php rm -v /snappymail/README.md /snappymail/_include.php
# Inspired by the original Rainloop dockerfile from youtous on GitLab # 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" LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
@ -99,9 +99,6 @@ RUN set -eux; \
# mysql # mysql
RUN docker-php-ext-install pdo_mysql RUN docker-php-ext-install pdo_mysql
# opcache
RUN docker-php-ext-install opcache
# postgres # postgres
RUN set -eux; \ RUN set -eux; \
apk add --no-cache postgresql-libs; \ apk add --no-cache postgresql-libs; \
@ -122,6 +119,13 @@ RUN set -eux; \
apk del .build-dependencies; \ apk del .build-dependencies; \
apk del .deps 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 # tidy
RUN set -eux; \ RUN set -eux; \
apk add --no-cache tidyhtml; \ apk add --no-cache tidyhtml; \

View file

@ -9,6 +9,12 @@ UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
MEMORY_LIMIT=${MEMORY_LIMIT:-128M} MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
SECURE_COOKIES=${SECURE_COOKIES:-true} 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 # Set attachment size limit
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf

View file

@ -52,6 +52,8 @@ http {
server { server {
listen 8888; listen 8888;
listen [::]:8888;
#listen [::]:8888 ipv6only=off;
root /snappymail; root /snappymail;
index index.php; index index.php;
charset utf-8; charset utf-8;

View file

@ -3,7 +3,7 @@ module.exports = {
// extends: ['eslint:recommended', 'plugin:prettier/recommended'], // extends: ['eslint:recommended', 'plugin:prettier/recommended'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
parserOptions: { parserOptions: {
ecmaVersion: 6, ecmaVersion: 11,
sourceType: 'module' sourceType: 'module'
}, },
env: { env: {
@ -37,7 +37,11 @@ module.exports = {
// Mailvelope // Mailvelope
'mailvelope': "readonly", 'mailvelope': "readonly",
// Punycode // Punycode
'IDN': "readonly" 'IDN': "readonly",
// Turndown
'TurndownService': "readonly",
// Marked
'marked': "readonly"
}, },
// http://eslint.org/docs/rules/ // http://eslint.org/docs/rules/
rules: { rules: {

View file

@ -30,8 +30,9 @@ If applicable, add screenshots to help explain your problem.
- SnappyMail Version: - SnappyMail Version:
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker] - Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)** **Debug/logging information**
Place them here (few lines) or as attachments (many lines) [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** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

2
.gitignore vendored
View file

@ -25,6 +25,8 @@
/vendors/knockout/spec /vendors/knockout/spec
/vendors/openpgp-5 /vendors/openpgp-5
!/vendors/openpgp-5/dist !/vendors/openpgp-5/dist
/vendors/squire2
!/vendors/squire2/dist
/vendors/vanillaqr.js/ /vendors/vanillaqr.js/
/integrations/nextcloud/rainloop /integrations/nextcloud/rainloop
/integrations/owncloud/rainloop /integrations/owncloud/rainloop

4
.gitmodules vendored
View file

@ -1,3 +1,7 @@
[submodule "vendors/openpgp-5"] [submodule "vendors/openpgp-5"]
path = vendors/openpgp-5 path = vendors/openpgp-5
url = git@github.com:the-djmaze/openpgpjs.git url = git@github.com:the-djmaze/openpgpjs.git
[submodule "vendors/squire2"]
path = vendors/squire2
url = git@github.com:the-djmaze/Squire.git
branch = snappymail

View file

@ -1,7 +1,15 @@
<FilesMatch "index\.php">
AcceptPathInfo On
</FilesMatch>
<IfModule mod_rewrite.c> <IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine On
# Redirect cPanel # Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301] RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
</IfModule> </IfModule>
<IfModule mod_expires.c> <IfModule mod_expires.c>

View file

@ -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 ## 2.35.4 – 2024-03-16
### Added ### Added
@ -6,7 +383,6 @@
### Changed ### Changed
- OpenPGP.js to v5.11.1 - OpenPGP.js to v5.11.1
- punycode.js lowercase domain names - punycode.js lowercase domain names
- Nextcloud changed stored password handling
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin` - application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
- Update Portuguese by @ner00 - Update Portuguese by @ner00
@ -21,6 +397,11 @@
[#1500](https://github.com/the-djmaze/snappymail/issues/1500) [#1500](https://github.com/the-djmaze/snappymail/issues/1500)
- VCard PHP Notice: Undefined index: ENCODING - 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 ## 2.35.3 – 2024-03-12
@ -68,8 +449,9 @@
- Crash on importing corrupt OpenPGP keys - Crash on importing corrupt OpenPGP keys
- Crash on old browsers instead of showing error - Crash on old browsers instead of showing error
- Ignore popups on logoutReload() - Ignore popups on logoutReload()
[#1467](https://github.com/the-djmaze/snappymail/issues/1467)
- Custom SASLMechanisms fail in IMAP when the connection is secure - 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 ## 2.35.2 – 2024-02-27
@ -86,7 +468,9 @@
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues - Drop support for gnupg PECL extension as it fails with "no passphrase" issues
- Error 352 when getting Filters - Error 352 when getting Filters
[#1455](https://github.com/the-djmaze/snappymail/issues/1455) [#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) [#1456](https://github.com/the-djmaze/snappymail/issues/1456)
@ -267,8 +651,8 @@
- Run full GetUids() in background when message_list_limit is set - Run full GetUids() in background when message_list_limit is set
- MessageListThreadsMap as background task when message_list_limit is set - MessageListThreadsMap as background task when message_list_limit is set
- Properly set CACHEDIR.TAG - Properly set CACHEDIR.TAG
- Sending group email to all contact addresses by @rezaei92 - Sending group email to all contact addresses
[#1286](https://github.com/the-djmaze/snappymail/pull/1286) [#1286](https://github.com/the-djmaze/snappymail/pull/1286) by @rezaei92
### Changed ### Changed
- Default IMAP message_list_limit to 10000 - Default IMAP message_list_limit to 10000
@ -276,8 +660,8 @@
- Improved Squire WYSIWYG - Improved Squire WYSIWYG
- Sort real attachments and inline attachments for - Sort real attachments and inline attachments for
[#1360](https://github.com/the-djmaze/snappymail/issues/1360) [#1360](https://github.com/the-djmaze/snappymail/issues/1360)
- Nextcloud Theme fixes and improvements by @hampoelz - Nextcloud Theme fixes and improvements
[#1363](https://github.com/the-djmaze/snappymail/pull/1363) [#1363](https://github.com/the-djmaze/snappymail/pull/1363) by @hampoelz
- Improve display of attachments - Improve display of attachments
[#1361](https://github.com/the-djmaze/snappymail/issues/1361) [#1361](https://github.com/the-djmaze/snappymail/issues/1361)
- Rename messageVisibility to messageVisible - Rename messageVisibility to messageVisible
@ -291,8 +675,8 @@
- Preview tooltip shows "null" when PREVIEW capability is disabled - Preview tooltip shows "null" when PREVIEW capability is disabled
### Nextcloud ### Nextcloud
- Improved language handling by @avinash-0007 - Improved language handling
[#1362](https://github.com/the-djmaze/snappymail/pull/1362) [#1362](https://github.com/the-djmaze/snappymail/pull/1362) by @avinash-0007
- FilterLanguage had wrong parameter order - FilterLanguage had wrong parameter order
- Use NextcloudV25+ theme by default - Use NextcloudV25+ theme by default
@ -313,8 +697,8 @@
[#1344](https://github.com/the-djmaze/snappymail/issues/1344) [#1344](https://github.com/the-djmaze/snappymail/issues/1344)
- NO Mailbox does not exist, or must be subscribed to") - NO Mailbox does not exist, or must be subscribed to")
[#1354](https://github.com/the-djmaze/snappymail/issues/1354) [#1354](https://github.com/the-djmaze/snappymail/issues/1354)
- Flag indicators are added to wrong message by @SergeyMosin - Flag indicators are added to wrong message
[#1347](https://github.com/the-djmaze/snappymail/pull/1347) [#1347](https://github.com/the-djmaze/snappymail/pull/1347) by @SergeyMosin
- Squire: issue when using the enter key in a reply window - Squire: issue when using the enter key in a reply window
[#1296](https://github.com/the-djmaze/snappymail/issues/1296) [#1296](https://github.com/the-djmaze/snappymail/issues/1296)
- Squire: crash on cut/delete range - Squire: crash on cut/delete range
@ -428,8 +812,8 @@
[#1308](https://github.com/the-djmaze/snappymail/issues/1308) [#1308](https://github.com/the-djmaze/snappymail/issues/1308)
- With email open, "delete" doesn't delete #1274 - With email open, "delete" doesn't delete #1274
[#1274](https://github.com/the-djmaze/snappymail/issues/1274) [#1274](https://github.com/the-djmaze/snappymail/issues/1274)
- Fix threading view in Thunderbird (others?) by @tkasch - Fix threading view in Thunderbird (others?)
[#1304](https://github.com/the-djmaze/snappymail/pull/1304) [#1304](https://github.com/the-djmaze/snappymail/pull/1304) by @tkasch
## 2.29.1 – 2023-10-02 ## 2.29.1 – 2023-10-02
@ -443,18 +827,18 @@
### Added ### Added
- Modern UI / Nextcloud Theme - Modern UI / Nextcloud Theme
[#629](https://github.com/the-djmaze/snappymail/pull/629) by @hampoelz [#629](https://github.com/the-djmaze/snappymail/pull/629) by @hampoelz
- "Add/Edit signature" label to PopupsIdentity.html by @SergeyMosin - "Add/Edit signature" label to PopupsIdentity.html
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#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 - 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) [#1248](https://github.com/the-djmaze/snappymail/pull/1248)
- Show unseen message count when the message list is threaded by @SergeyMosin - Show unseen message count when the message list is threaded
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- in mobile mode hide folders(left) panel when a folder is clicked by @SergeyMosin - in mobile mode hide folders(left) panel when a folder is clicked
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- spellcheck the subject when 'allowSpellcheck' setting is true by @SergeyMosin - spellcheck the subject when 'allowSpellcheck' setting is true
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section) by @SergeyMosin - 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section)
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- Browser support for autocompleting TOTP code - Browser support for autocompleting TOTP code
[#1251](https://github.com/the-djmaze/snappymail/issues/1251) [#1251](https://github.com/the-djmaze/snappymail/issues/1251)
@ -465,10 +849,10 @@
[#1199](https://github.com/the-djmaze/snappymail/issues/1199) [#1199](https://github.com/the-djmaze/snappymail/issues/1199)
- Draft code to improve mobile breakpoints - Draft code to improve mobile breakpoints
[#1150](https://github.com/the-djmaze/snappymail/issues/1150) [#1150](https://github.com/the-djmaze/snappymail/issues/1150)
- address input: space character can trigger '_parseValue' if the email address looks complete by @SergeyMosin - address input: space character can trigger '_parseValue' if the email address looks complete
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#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 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) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
### Fixed ### Fixed
- CHARSET is not valid in UTF8 mode - CHARSET is not valid in UTF8 mode
@ -481,39 +865,39 @@
[#1220](https://github.com/the-djmaze/snappymail/issues/1220) [#1220](https://github.com/the-djmaze/snappymail/issues/1220)
- "Request failed" on message move - "Request failed" on message move
[#1220](https://github.com/the-djmaze/snappymail/issues/1220) [#1220](https://github.com/the-djmaze/snappymail/issues/1220)
- Unwrapped text nodes attached to squire._root by @SergeyMosin - Unwrapped text nodes attached to squire._root
[#1234](https://github.com/the-djmaze/snappymail/pull/1234) [#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. - 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) [#1208](https://github.com/the-djmaze/snappymail/issues/1208)
- foreach() argument must be of type array|object - foreach() argument must be of type array|object
[#1237](https://github.com/the-djmaze/snappymail/issues/1237) [#1237](https://github.com/the-djmaze/snappymail/issues/1237)
- `<font>` tag 'style' is lost in replies by @SergeyMosin - `<font>` tag 'style' is lost in replies
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- unseen indicator is not shown in thread view when 'listGrouped' settings is false by @SergeyMosin - unseen indicator is not shown in thread view when 'listGrouped' settings is false
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- TOTP plugin is dependent on ctype - TOTP plugin is dependent on ctype
[#1250](https://github.com/the-djmaze/snappymail/issues/1250) [#1250](https://github.com/the-djmaze/snappymail/issues/1250)
### Nextcloud ### Nextcloud
- iFrame mode: click on unified search result opens inner iFrame by @SergeyMosin - iFrame mode: click on unified search result opens inner iFrame
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) [#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. 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) [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
- Improve UX of "Put in Calendar" option in plugin by @theronakpatel - Improve UX of "Put in Calendar" option in plugin
[#1259](https://github.com/the-djmaze/snappymail/pull/1259) [#1259](https://github.com/the-djmaze/snappymail/pull/1259) by @theronakpatel
## 2.28.4 – 2023-07-10 ## 2.28.4 – 2023-07-10
### Added ### Added
- application.ini msg_default_action by @SergeyMosin - application.ini msg_default_action
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- application.ini view_show_next_message by @SergeyMosin - application.ini view_show_next_message
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- application.ini view_images by @SergeyMosin - application.ini view_images
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- nextcloud add ability to include custom php file in InstallStep migration by @SergeyMosin - nextcloud add ability to include custom php file in InstallStep migration
[#1197](https://github.com/the-djmaze/snappymail/pull/1197) [#1197](https://github.com/the-djmaze/snappymail/pull/1197) by @SergeyMosin
- Support plugin for Squire editor - Support plugin for Squire editor
[#1192](https://github.com/the-djmaze/snappymail/issues/1192) [#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). - 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) [#1204](https://github.com/the-djmaze/snappymail/issues/1204)
- Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance - Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance
- New bidi buttons to Squire editor by @rezaei92 - New bidi buttons to Squire editor
[#1200](https://github.com/the-djmaze/snappymail/pull/1200) [#1200](https://github.com/the-djmaze/snappymail/pull/1200) by @rezaei92
### Fixed ### Fixed
- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT - Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT
[#1205](https://github.com/the-djmaze/snappymail/issues/1205) [#1205](https://github.com/the-djmaze/snappymail/issues/1205)
- 'reloadTime' function result is passed into 'setInterval' instead of the function by @SergeyMosin - 'reloadTime' function result is passed into 'setInterval' instead of the function
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
- UNKNOWN-CTE Invalid data in MIME part - UNKNOWN-CTE Invalid data in MIME part
[#1186](https://github.com/the-djmaze/snappymail/issues/1186) [#1186](https://github.com/the-djmaze/snappymail/issues/1186)
@ -591,10 +975,10 @@
### Changed ### Changed
- Optical issue with input fields for mail and folder search - Optical issue with input fields for mail and folder search
[#1149](https://github.com/the-djmaze/snappymail/issues/1149) [#1149](https://github.com/the-djmaze/snappymail/issues/1149)
- Update Chinese translation by @mayswind - Update Chinese translation
[#1157](https://github.com/the-djmaze/snappymail/pull/1157) [#1157](https://github.com/the-djmaze/snappymail/pull/1157) by @mayswind
- Update Polish translation by @tinola - Update Polish translation
[#1156](https://github.com/the-djmaze/snappymail/pull/1156) [#1156](https://github.com/the-djmaze/snappymail/pull/1156) by @tinola
### Fixed ### Fixed
- Undefined SIG constants - Undefined SIG constants
@ -623,18 +1007,18 @@
- Allow empty message body when there are attachments - Allow empty message body when there are attachments
[#1052](https://github.com/the-djmaze/snappymail/issues/1052) [#1052](https://github.com/the-djmaze/snappymail/issues/1052)
- PHP inherit logger as Trait - PHP inherit logger as Trait
- Update Portuguese by @ner00 - Update Portuguese
[#1124](https://github.com/the-djmaze/snappymail/pull/1124) [#1124](https://github.com/the-djmaze/snappymail/pull/1124) by @ner00
- Update Traditional Chinese (Taiwan) by @chiyi4488 - Update Traditional Chinese (Taiwan)
[#1107](https://github.com/the-djmaze/snappymail/pull/1107) [#1107](https://github.com/the-djmaze/snappymail/pull/1107) by @chiyi4488
- Update Russian by @konkere - Update Russian
[#1108](https://github.com/the-djmaze/snappymail/pull/1108) [#1108](https://github.com/the-djmaze/snappymail/pull/1108) by @konkere
- Update Italian by @cm-schl - Update Italian
[#1094](https://github.com/the-djmaze/snappymail/pull/1094) [#1094](https://github.com/the-djmaze/snappymail/pull/1094) by @cm-schl
- Update French by @hguilbert - Update French
[#1102](https://github.com/the-djmaze/snappymail/pull/1102) [#1102](https://github.com/the-djmaze/snappymail/pull/1102) by @hguilbert
- Update German by @cm-schl - Update German
[#1087](https://github.com/the-djmaze/snappymail/pull/1087) [#1087](https://github.com/the-djmaze/snappymail/pull/1087) by @cm-schl
### Fixed ### Fixed
- Show messagelist timeout/abort error for - Show messagelist timeout/abort error for
@ -695,8 +1079,8 @@
- Workaround Upgrade with Nextcloud stuck in Maintenance mode - Workaround Upgrade with Nextcloud stuck in Maintenance mode
[#1046](https://github.com/the-djmaze/snappymail/issues/1046) [#1046](https://github.com/the-djmaze/snappymail/issues/1046)
- Use Actions()->decodeRawKey() - Use Actions()->decodeRawKey()
- French language updated by @hguilbert - French language updated
[#1045](https://github.com/the-djmaze/snappymail/pull/1045) [#1045](https://github.com/the-djmaze/snappymail/pull/1045) by @hguilbert
- Sorting in folders not working - Sorting in folders not working
[#1022](https://github.com/the-djmaze/snappymail/issues/1022) [#1022](https://github.com/the-djmaze/snappymail/issues/1022)
- Allow saving draft with empty body - Allow saving draft with empty body
@ -760,15 +1144,15 @@
### Changed ### Changed
- Improved FolderCollection handling - Improved FolderCollection handling
- MODSEQ requires 64-bit int - MODSEQ requires 64-bit int
- Update russian language by @Akrobs - Update russian language
[#994](https://github.com/the-djmaze/snappymail/pull/994) [#994](https://github.com/the-djmaze/snappymail/pull/994) by @Akrobs
- Don't make font bigger when screen > 1400px - Don't make font bigger when screen > 1400px
- Put top menu "accountPlace" inside top-system-dropdown-id - Put top menu "accountPlace" inside top-system-dropdown-id
- Put attachment controls inside attachmentsPlace - Put attachment controls inside attachmentsPlace
- Show message toolbar on screens > 1400px - Show message toolbar on screens > 1400px
[#970](https://github.com/the-djmaze/snappymail/issues/970) [#970](https://github.com/the-djmaze/snappymail/issues/970)
- Chinese updated by @mayswind - Chinese updated
[#1011](https://github.com/the-djmaze/snappymail/pull/1011) [#1011](https://github.com/the-djmaze/snappymail/pull/1011) by @mayswind
- Prevent folder/messages flags conflict by using the right name `attributes` for Folders - Prevent folder/messages flags conflict by using the right name `attributes` for Folders
- FolderInformation() use jsonSerialize() - FolderInformation() use jsonSerialize()
- Squire space handling on paste use `\u00A0` instead of `&nbsp;` for - Squire space handling on paste use `\u00A0` instead of `&nbsp;` for
@ -811,16 +1195,16 @@
### Changed ### Changed
- Reduce/simplify CSS footprint - Reduce/simplify CSS footprint
- Use the System/Browser font by default by @HeySora - Use the System/Browser font by default
[#988](https://github.com/the-djmaze/snappymail/pull/988) [#988](https://github.com/the-djmaze/snappymail/pull/988) by @HeySora
- Make layout fully responsive using matchMedia('(max-width: 799px)') - Make layout fully responsive using matchMedia('(max-width: 799px)')
- Move brotli and gzip compress option to application.ini - Move brotli and gzip compress option to application.ini
- After page refreshes in background whilst editing a draft, the space bar stops working - After page refreshes in background whilst editing a draft, the space bar stops working
[#860](https://github.com/the-djmaze/snappymail/issues/860) [#860](https://github.com/the-djmaze/snappymail/issues/860)
- Updated Portuguese by @ner00 - Updated Portuguese
[#984](https://github.com/the-djmaze/snappymail/pull/984) [#984](https://github.com/the-djmaze/snappymail/pull/984) by @ner00
- Updated French by @hguilbert - Updated French
[#985](https://github.com/the-djmaze/snappymail/pull/985) [#985](https://github.com/the-djmaze/snappymail/pull/985) by @hguilbert
### Fixed ### Fixed
- unset border-box for message body - unset border-box for message body
@ -916,8 +1300,8 @@
- Workaround "server connection error" - Workaround "server connection error"
[#936](https://github.com/the-djmaze/snappymail/issues/936) [#936](https://github.com/the-djmaze/snappymail/issues/936)
- Cleanup AbstractViewPopup handling - Cleanup AbstractViewPopup handling
- Italian translation by @lota - Italian translation
[#948](https://github.com/the-djmaze/snappymail/pull/948) [#948](https://github.com/the-djmaze/snappymail/pull/948) by @lota
- Replace Element.fromHTML() with createElement() - Replace Element.fromHTML() with createElement()
### Fixed ### Fixed
@ -965,8 +1349,8 @@
## 2.25.5 – 2023-02-09 ## 2.25.5 – 2023-02-09
### Added ### Added
- New dark themes by @TheCuteFoxxy - New dark themes
[#925](https://github.com/the-djmaze/snappymail/pull/925) [#925](https://github.com/the-djmaze/snappymail/pull/925) by @TheCuteFoxxy
- External images option 'Always when DKIM is valid' for - External images option 'Always when DKIM is valid' for
[#201](https://github.com/the-djmaze/snappymail/issues/201) [#201](https://github.com/the-djmaze/snappymail/issues/201)
- Image whitelist menu on message for - Image whitelist menu on message for
@ -1006,8 +1390,8 @@
### Added ### Added
- message maximum quoted text level for speed. - message maximum quoted text level for speed.
[#902](https://github.com/the-djmaze/snappymail/issues/902) [#902](https://github.com/the-djmaze/snappymail/issues/902)
- LoveDark Theme by @TheCuteFoxxy - LoveDark Theme
[#913](https://github.com/the-djmaze/snappymail/pull/913) [#913](https://github.com/the-djmaze/snappymail/pull/913) by @TheCuteFoxxy
### Changed ### Changed
- Moved pluginEnable() to \SnappyMail\Repository::enablePackage() - Moved pluginEnable() to \SnappyMail\Repository::enablePackage()
@ -1089,8 +1473,8 @@
[#188](https://github.com/the-djmaze/snappymail/issues/188) [#188](https://github.com/the-djmaze/snappymail/issues/188)
- /static/.htaccess as mentioned by @dbiczo in - /static/.htaccess as mentioned by @dbiczo in
[#895](https://github.com/the-djmaze/snappymail/issues/895) [#895](https://github.com/the-djmaze/snappymail/issues/895)
- Update nb-NO language by @master3395 - Update nb-NO language
[#896](https://github.com/the-djmaze/snappymail/pull/896) [#896](https://github.com/the-djmaze/snappymail/pull/896) by @master3395
- \RainLoop\Utils::SetCookie() to \SnappyMail\Cookies::set() - \RainLoop\Utils::SetCookie() to \SnappyMail\Cookies::set()
- Merge \MailSo\Base\Utils::RecRmDir() and \MailSo\Base\Utils::RecTimeDirRemove() - Merge \MailSo\Base\Utils::RecRmDir() and \MailSo\Base\Utils::RecTimeDirRemove()
@ -1115,8 +1499,8 @@
- Support RFC 8689 - Support RFC 8689
- Nextcloud move themes outside of app folder - Nextcloud move themes outside of app folder
[#875](https://github.com/the-djmaze/snappymail/issues/875) [#875](https://github.com/the-djmaze/snappymail/issues/875)
- Add check to unregister system addressbook by @akhil1508 - Add check to unregister system addressbook
[#879](https://github.com/the-djmaze/snappymail/pull/879) [#879](https://github.com/the-djmaze/snappymail/pull/879) by @akhil1508
### Changed ### Changed
- material-design checkbox use hidden input for accessibility - material-design checkbox use hidden input for accessibility
@ -1140,8 +1524,8 @@
## 2.25.0 – 2023-01-26 ## 2.25.0 – 2023-01-26
### Changed ### Changed
- Right margin to folder search input wrapper by @codiflow - Right margin to folder search input wrapper
[#871](https://github.com/the-djmaze/snappymail/pull/871) [#871](https://github.com/the-djmaze/snappymail/pull/871) by @codiflow
- Almost all request params now use JavaScript camelCase instead of CamelCase - Almost all request params now use JavaScript camelCase instead of CamelCase
- #rl-content to use flex for better layout control - #rl-content to use flex for better layout control
- Decode some Copernica link tracking - Decode some Copernica link tracking
@ -1170,8 +1554,8 @@
### Added ### Added
- Preparations for RFC 8689 - Preparations for RFC 8689
- Add admin setting for the contacts suggestions limit - Add admin setting for the contacts suggestions limit
- A button to clear folder search input field by @codiflow - A button to clear folder search input field
[#847](https://github.com/the-djmaze/snappymail/pull/847) [#847](https://github.com/the-djmaze/snappymail/pull/847) by @codiflow
- Preparations for custom SMTP credentials handling for - Preparations for custom SMTP credentials handling for
[#859](https://github.com/the-djmaze/snappymail/issues/859) [#859](https://github.com/the-djmaze/snappymail/issues/859)
[#458](https://github.com/the-djmaze/snappymail/issues/458) [#458](https://github.com/the-djmaze/snappymail/issues/458)
@ -1187,8 +1571,8 @@
[#849](https://github.com/the-djmaze/snappymail/issues/849) [#849](https://github.com/the-djmaze/snappymail/issues/849)
- 64-bit PHP was required - 64-bit PHP was required
[#852](https://github.com/the-djmaze/snappymail/issues/852) [#852](https://github.com/the-djmaze/snappymail/issues/852)
- Update fr-FR translations by @dominiquefournier - Update fr-FR translations
[#854](https://github.com/the-djmaze/snappymail/pull/854) [#854](https://github.com/the-djmaze/snappymail/pull/854) by @dominiquefournier
- Move createDomain & createDomainAlias buttons below table - Move createDomain & createDomainAlias buttons below table
- Move some Nextcloud SnappyMailHelper code to new InstallStep - Move some Nextcloud SnappyMailHelper code to new InstallStep
- Cleanup messages cache handling - Cleanup messages cache handling
@ -1214,8 +1598,8 @@
### Added ### Added
- Support for search criterias ON, SENTON, SENTSINCE and SENTBEFORE - Support for search criterias ON, SENTON, SENTSINCE and SENTBEFORE
- New 'forward as attachment' glyph - New 'forward as attachment' glyph
- NC integration app metadata: add links to admin and dev docs by @p-bo - NC integration app metadata: add links to admin and dev docs
[#820](https://github.com/the-djmaze/snappymail/pull/820) [#820](https://github.com/the-djmaze/snappymail/pull/820) by @p-bo
- Clicking on messagelist grouped date/from searches on that date/from - Clicking on messagelist grouped date/from searches on that date/from
[#815](https://github.com/the-djmaze/snappymail/issues/815) [#815](https://github.com/the-djmaze/snappymail/issues/815)
- Nextcloud Improved file picker layout - Nextcloud Improved file picker layout
@ -1229,16 +1613,16 @@
- Resolve layout issues by changing from `fixed` to `relative` - Resolve layout issues by changing from `fixed` to `relative`
[#686](https://github.com/the-djmaze/snappymail/issues/686) [#686](https://github.com/the-djmaze/snappymail/issues/686)
- Cache handling of messagelist changed due to etag issues - Cache handling of messagelist changed due to etag issues
- Improve german and italian translation by @cm-schl - Improve german and italian translation
[#846](https://github.com/the-djmaze/snappymail/pull/846) [#846](https://github.com/the-djmaze/snappymail/pull/846) by @cm-schl
### Fixed ### Fixed
- Searching on Nextcloud search - Searching on Nextcloud search
[#787](https://github.com/the-djmaze/snappymail/issues/787) [#787](https://github.com/the-djmaze/snappymail/issues/787)
- Workaround another Nextcloud disallowed .htaccess - Workaround another Nextcloud disallowed .htaccess
[#790](https://github.com/the-djmaze/snappymail/issues/790) [#790](https://github.com/the-djmaze/snappymail/issues/790)
- Compact display of folders in nextcloud by @makoehr - Compact display of folders in nextcloud
[#824](https://github.com/the-djmaze/snappymail/pull/824) [#824](https://github.com/the-djmaze/snappymail/pull/824) by @makoehr
- Admin -> Contacts PDO test failed when using different AddressBookInterface - Admin -> Contacts PDO test failed when using different AddressBookInterface
- KolabAddressBook errors - KolabAddressBook errors
- Forgot debug js/css setting change in Nextcloud - Forgot debug js/css setting change in Nextcloud
@ -1290,8 +1674,8 @@
### Changed ### Changed
- Disable sorting when viewing message thread - Disable sorting when viewing message thread
[#445](https://github.com/the-djmaze/snappymail/issues/445) [#445](https://github.com/the-djmaze/snappymail/issues/445)
- Update Chinese translation by @mayswind - Update Chinese translation
[#794](https://github.com/the-djmaze/snappymail/pull/794) [#794](https://github.com/the-djmaze/snappymail/pull/794) by @mayswind
- No need to call IMAP EXAMINE when current folder already SELECT - No need to call IMAP EXAMINE when current folder already SELECT
- Thread view now has tree indentation - Thread view now has tree indentation
@ -1404,8 +1788,8 @@
### Fixed ### Fixed
- Loosing HTML signature in account identity under settings - Loosing HTML signature in account identity under settings
[#750](https://github.com/the-djmaze/snappymail/issues/750) [#750](https://github.com/the-djmaze/snappymail/issues/750)
- Plugin configuration did not load anymore when type was SELECTION by @cm-schl - Plugin configuration did not load anymore when type was SELECTION
[#753](https://github.com/the-djmaze/snappymail/pull/753) [#753](https://github.com/the-djmaze/snappymail/pull/753) by @cm-schl
- Nextcloud Default theme shows gray text on gray background - Nextcloud Default theme shows gray text on gray background
[#754](https://github.com/the-djmaze/snappymail/issues/754) [#754](https://github.com/the-djmaze/snappymail/issues/754)
- Only run JSON hooks when $sAction is set - Only run JSON hooks when $sAction is set
@ -1669,8 +2053,8 @@
[#638](https://github.com/the-djmaze/snappymail/issues/638) [#638](https://github.com/the-djmaze/snappymail/issues/638)
### Fixed ### Fixed
- ldap-identities-plugin by @cm-schl - ldap-identities-plugin
[#647](https://github.com/the-djmaze/snappymail/pull/647) [#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
- OpenSSL v3 ciphers issue - OpenSSL v3 ciphers issue
[#641](https://github.com/the-djmaze/snappymail/issues/641) [#641](https://github.com/the-djmaze/snappymail/issues/641)
@ -1769,8 +2153,8 @@
[#620](https://github.com/the-djmaze/snappymail/issues/620) [#620](https://github.com/the-djmaze/snappymail/issues/620)
- PutinICS does is not working for all calendar events - PutinICS does is not working for all calendar events
[#622](https://github.com/the-djmaze/snappymail/issues/622) [#622](https://github.com/the-djmaze/snappymail/issues/622)
- Update readme by @cm-schl - Update readme
[#617](https://github.com/the-djmaze/snappymail/pull/617) [#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
## 2.20.0 – 2022-11-03 ## 2.20.0 – 2022-11-03
@ -1864,8 +2248,8 @@
[#638](https://github.com/the-djmaze/snappymail/issues/638) [#638](https://github.com/the-djmaze/snappymail/issues/638)
### Fixed ### Fixed
- ldap-identities-plugin by @cm-schl - ldap-identities-plugin
[#647](https://github.com/the-djmaze/snappymail/pull/647) [#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
- OpenSSL v3 ciphers issue - OpenSSL v3 ciphers issue
[#641](https://github.com/the-djmaze/snappymail/issues/641) [#641](https://github.com/the-djmaze/snappymail/issues/641)
@ -1964,8 +2348,8 @@
[#620](https://github.com/the-djmaze/snappymail/issues/620) [#620](https://github.com/the-djmaze/snappymail/issues/620)
- PutinICS does is not working for all calendar events - PutinICS does is not working for all calendar events
[#622](https://github.com/the-djmaze/snappymail/issues/622) [#622](https://github.com/the-djmaze/snappymail/issues/622)
- Update readme by @cm-schl - Update readme
[#617](https://github.com/the-djmaze/snappymail/pull/617) [#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
## 2.20.0 – 2022-11-03 ## 2.20.0 – 2022-11-03

View file

@ -49,7 +49,7 @@ This fork of RainLoop has the following changes:
* Admin uses password_hash/password_verify * Admin uses password_hash/password_verify
* Auth failed attempts written to syslog * Auth failed attempts written to syslog
* Added Fail2ban instructions * Added Fail2ban instructions
* ES2018 * ES2020
* PHP 7.4+ required * PHP 7.4+ required
* PHP mbstring extension required * PHP mbstring extension required
* PHP replaced pclZip with PharData and ZipArchive * PHP replaced pclZip with PharData and ZipArchive
@ -66,7 +66,7 @@ This fork of RainLoop has the following changes:
* CRLF => LF line endings * CRLF => LF line endings
* Embed boot.js and boot.css into index.html * Embed boot.js and boot.css into index.html
* Removal of old JavaScript code (things are native these days) * 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) * Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
* Split Admin specific JavaScript code from User code * Split Admin specific JavaScript code from User code
* Split Sieve 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 | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.170.153 | 82.845 | |admin.js |2.170.153 | 84.925 |
|app.js |4.207.787 | 430.201 | |app.js |4.207.787 | 447.263 |
|boot.js | 868.735 | 4.147 | |boot.js | 868.735 | 4.343 |
|libs.js | 658.812 | 201.911 | |libs.js | 658.812 | 233.728 |
|sieve.js | 0 | 84.703 | |sieve.js | 0 | 91.418 |
|polyfills.js | 334.608 | 0 | |polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 | |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 | |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 | |admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|app.min.js | 515.367 | 195.260 |139.456 | 66.275 |110.485 | 56.682 | |app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 | |boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|libs.min.js | 584.772 | 92.482 |180.901 | 34.729 |155.182 | 31.017 | |libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 | |sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | |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 |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|TOTAL user+sieve |1.217.635 | 330.993 |358.761 |112.573 |299.485 | 98.060 | |TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
|TOTAL admin | 959.099 | 135.302 |292.911 | 49.582 |249.877 | 44.247 | |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 ### 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 | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 84.531 | 46.946 | 17.627 | 15.111 | |app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|app.min.css | 274.947 | 67.906 | 39.647 | 15.544 | 13.567 | |app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|boot.css | | 1.326 | | 664 | 545 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.573 | | 7.011 | 6.101 | |admin.css | | 30.880 | | 7.045 | 6.127 |
|admin.min.css | | 24.689 | | 6.334 | 5.575 | |admin.min.css | | 24.959 | | 6.368 | 5.615 |
### PGP ### PGP
RainLoop uses the old OpenPGP.js v2 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 |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 | | |openpgp.worker | 1.499 | | 824 | | 695 | |

View file

@ -6,8 +6,8 @@ Currently due to the fast development only the latest version receives security
| Version | Supported | | Version | Supported |
| -------- | --------- | | -------- | --------- |
| 2.30.x | ✔ | | 2.34.x | ✔ |
| < 2.30.0 | ❌ | | < 2.34.0 | ❌ |
## Reporting a Vulnerability ## Reporting a Vulnerability
@ -36,5 +36,5 @@ YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
## Publishing and Credits ## Publishing and Credits
I will analyze and fix the reported issue as fast as possible. 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. Credits to the reporter are granted and can be included in all public communication if desired.

View file

@ -9,7 +9,7 @@ if (isset($options['plugins'])) {
require(ROOT_DIR . '/build/plugins.php'); require(ROOT_DIR . '/build/plugins.php');
} }
$gulp = trim(`which gulp`); $gulp = trim(shell_exec('which gulp'));
if (!$gulp) { if (!$gulp) {
exit('gulp not installed, run as root: npm install --global gulp-cli'); exit('gulp not installed, run as root: npm install --global gulp-cli');
} }
@ -34,7 +34,7 @@ $options['aur'] = isset($options['aur']);
// Docker build // Docker build
$options['docker'] = isset($options['docker']); $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'); exit('docker not found');
} }
if ($options['docker'] && $options['aur']) { if ($options['docker'] && $options['aur']) {
@ -60,7 +60,7 @@ if (!isset($options['skip-gulp'])) {
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static'; $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"; echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
passthru("{$gzip} -k --best {$cmddir}/js/*.js"); passthru("{$gzip} -k --best {$cmddir}/js/*.js");
passthru("{$gzip} -k --best {$cmddir}/js/min/*.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'); 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"; echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
passthru("{$brotli} -k --best {$cmddir}/js/*.js"); passthru("{$brotli} -k --best {$cmddir}/js/*.js");
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js"); passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
@ -188,7 +188,7 @@ file_put_contents("{$destPath}core.json", '{
// Arch User Repository // Arch User Repository
if ($options['aur']) { if ($options['aur']) {
// extension_loaded('blake2') // extension_loaded('blake2')
if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) { if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
function b2sum($file) { function b2sum($file) {
$file = escapeshellarg($file); $file = escapeshellarg($file);
exec("b2sum --binary {$file} 2>&1", $output, $exitcode); exec("b2sum --binary {$file} 2>&1", $output, $exitcode);

View file

@ -1,7 +1,7 @@
import 'External/User/ko'; import 'External/User/ko';
import { SMAudio } from 'Common/Audio'; import { SMAudio } from 'Common/Audio';
import { pInt } from 'Common/Utils'; import { arrayLength } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser'; import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
import { import {
@ -27,7 +27,7 @@ import {
getFolderFromCacheList getFolderFromCacheList
} from 'Common/Cache'; } from 'Common/Cache';
import { i18n, reloadTime } from 'Common/Translator'; import { i18n, reloadTime, getErrorMessage } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { NotificationUserStore } from 'Stores/User/Notification'; import { NotificationUserStore } from 'Stores/User/Notification';
@ -88,6 +88,8 @@ export class AppUser extends AbstractApp {
this.messageList = MessagelistUserStore; this.messageList = MessagelistUserStore;
this.ask = AskPopupView; this.ask = AskPopupView;
this.loadAccountsAndIdentities = loadAccountsAndIdentities;
} }
/** /**
@ -112,7 +114,7 @@ export class AppUser extends AbstractApp {
case FolderType.Trash: case FolderType.Trash:
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder()); oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
nSetSystemFoldersNotification = iFolderType; nSetSystemFoldersNotification = iFolderType;
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder() bDelete = bDelete/* || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()*/
|| sFromFolderFullName === FolderUserStore.spamFolder() || sFromFolderFullName === FolderUserStore.spamFolder()
|| sFromFolderFullName === FolderUserStore.trashFolder(); || sFromFolderFullName === FolderUserStore.trashFolder();
break; break;
@ -147,8 +149,10 @@ export class AppUser extends AbstractApp {
} }
logout() { logout() {
localStorage.removeItem('register_protocol_offered'); Remote.request('Logout', (iError, data) =>
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink'))); iError ? alert('Logout error: ' + getErrorMessage(iError, data))
: rl.logoutReload(Settings.app('customLogoutLink'))
);
} }
bootstart() { bootstart() {
@ -180,15 +184,15 @@ export class AppUser extends AbstractApp {
SettingsUserStore.init(); SettingsUserStore.init();
ContactUserStore.init(); ContactUserStore.init();
loadFolders(value => { loadFolders((success, error) => {
try { try {
if (value) { if (success) {
startScreens([ startScreens([
MailBoxUserScreen, MailBoxUserScreen,
SettingsUserScreen SettingsUserScreen
]); ]);
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval'))); setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
loadAccountsAndIdentities(); loadAccountsAndIdentities();
@ -222,19 +226,9 @@ export class AppUser extends AbstractApp {
SMimeUserStore.loadCertificates(); SMimeUserStore.loadCertificates();
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500); 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 { } else {
this.logout(); this.logout();
alert('Folders error: ' + getErrorMessage(0, error))
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -247,18 +241,31 @@ export class AppUser extends AbstractApp {
showMessageComposer(params = []) 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 => { return new Promise(resolve => {
this.showModal([ this.showModal([
sAskDesc, 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), () => resolve(null),
true, true,
5, ask || 1,
btnText btnText
]); ]);
}); });

View file

@ -71,7 +71,9 @@ MessageSetAction = {
SetSeen: 0, SetSeen: 0,
UnsetSeen: 1, UnsetSeen: 1,
SetFlag: 2, SetFlag: 2,
UnsetFlag: 3 UnsetFlag: 3,
SetDeleted: 4,
UnsetDeleted: 5
}, },
/** /**

View file

@ -1,7 +1,7 @@
/* eslint key-spacing: 0 */ /* eslint key-spacing: 0 */
/* eslint quote-props: 0 */ /* eslint quote-props: 0 */
import { arrayLength } from 'Common/Utils'; import { arrayLength, pInt } from 'Common/Utils';
export const RFC822 = 'message/rfc822'; export const RFC822 = 'message/rfc822';
@ -274,8 +274,7 @@ export const FileInfo = {
}, },
friendlySize: bytes => { friendlySize: bytes => {
bytes = parseInt(bytes, 10); bytes = pInt(bytes);
bytes = isFinite(bytes) ? bytes : 0;
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0; 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]; return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
} }

View file

@ -1,6 +1,8 @@
import { RFC822 } from 'Common/File'; import { RFC822 } from 'Common/File';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache'; 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 { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
@ -8,13 +10,13 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
let refreshInterval, let refreshInterval,
// Default every 5 minutes // Default every 15 minutes
refreshFoldersInterval = 300000; refreshFoldersInterval = 900000;
export const export const
setRefreshFoldersInterval = minutes => { setRefreshFoldersInterval = minutes => {
refreshFoldersInterval = Math.max(5, minutes) * 60000; refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
clearInterval(refreshInterval); clearInterval(refreshInterval);
refreshInterval = setInterval(() => { refreshInterval = setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(), const cF = FolderUserStore.currentFolderFullName(),
@ -27,7 +29,7 @@ setRefreshFoldersInterval = minutes => {
sortFolders = folders => { sortFolders = folders => {
try { try {
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); let collator = baseCollator(true);
folders.sort((a, b) => folders.sort((a, b) =>
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName)) 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('folder', sFolderFullName);
data.append('appendFile', file); data.append('appendFile', file);
Remote.request('FolderAppend', (iError, data)=>{ Remote.request('FolderAppend', (iError, data)=>{
iError && console.error(data.ErrorMessage); iError && console.error(data.message);
0 == --count 0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName && FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true); && MessagelistUserStore.reload(true, true);

View file

@ -4,6 +4,9 @@ import { SettingsUserStore } from 'Stores/User/Settings';
const const
tmpl = createElement('template'), tmpl = createElement('template'),
turndown = new TurndownService(),
htmlre = /[&<>"']/g, htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i, httpre = /^(https?:)?\/\//i,
htmlmap = { htmlmap = {
@ -14,14 +17,34 @@ const
"'": '&#x27;' "'": '&#x27;'
}, },
disallowedTags = [ keepTagContent = 'form,button,data', // font
'svg','script','title','link','base','meta',
'input','output','select','button','textarea', allowedTags = [
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map' // Structural Elements:
// not supported by <template> element 'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
// ,'html','head','body' '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(','), ].join(','),
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
],
blockquoteSwitcher = () => { blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() && SettingsUserStore.collapseBlockquotes() &&
// tmpl.content.querySelectorAll('blockquote').forEach(node => { // tmpl.content.querySelectorAll('blockquote').forEach(node => {
@ -99,8 +122,10 @@ const
}, },
cleanCSS = source => cleanCSS = source =>
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '') source.trim()
.replace(/white-space[^;]+(;|$)/g, '') .replace(/;\s*-[^;]+/g, '')
.replace(/^\s*-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+/g, '')
// Drop Microsoft Office style properties // Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '') // .replace(/mso-[^:;]+:[^;]+/gi, '')
, ,
@ -142,14 +167,15 @@ const
if (source) { if (source) {
source = source source = source
// strip comments // strip comments
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '') .replace(/\/\*[\s\S]*?\*\//gi, '')
// strip import statements // strip MS Word comments
.replace(/@import .*?;/gi , '') .replace(/<!--[\s\S]*?-->/gi, '')
// strip keyframe statements // strip HTML, as < is no CSS combinator anyway
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, ''); .replace(/<[\s\S]*/gi, '');
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
// unified regex to match css & media queries together // 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; arr;
while (true) { while (true) {
@ -158,7 +184,7 @@ const
break; 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 // Never have more than a single line break in a row
.replace(/\n+/, "\n") .replace(/\n+/, "\n")
// Remove :root and html // Remove :root and html
@ -170,13 +196,14 @@ const
css.push({ css.push({
selector: selector, selector: selector,
type: 'media', 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('@')) { } else if (selector && !selector.includes('@')) {
// we have standard css // we have standard css
// ignores @import, @keyframe, @font-face statements
css.push({ css.push({
selector: selector, selector: selector,
rules: cleanCSS(arr[6]) rules: cleanCSS(arr[4])
}); });
} }
} }
@ -205,6 +232,7 @@ export const
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'), detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()), bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = { result = {
hasExternals: false, hasExternals: false,
@ -212,6 +240,8 @@ export const
linkedData: [] linkedData: []
}, },
isMsg = !!msgId,
findAttachmentByCid = cId => oAttachments.findByCid(cId), findAttachmentByCid = cId => oAttachments.findByCid(cId),
findLocationByCid = cId => { findLocationByCid = cId => {
const attachment = findAttachmentByCid(cId); const attachment = findAttachmentByCid(cId);
@ -228,6 +258,15 @@ export const
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px' rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
}, },
allowedAttributes = [ 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 // defaults
'name', 'name',
'dir', 'lang', 'style', 'title', 'dir', 'lang', 'style', 'title',
@ -255,9 +294,8 @@ export const
'abbr', 'scope', 'abbr', 'scope',
// td // td
'colspan', 'rowspan', 'headers' 'colspan', 'rowspan', 'headers'
], // others
nonEmptyTags = [ //'class', 'id', 'target'
'A','B','EM','I','SPAN','STRONG'
]; ];
if (SettingsUserStore.allowStyles()) { if (SettingsUserStore.allowStyles()) {
@ -271,6 +309,8 @@ export const
// Not supported by <template> element // Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '') // .replace(/<!doctype[^>]*>/gi, '')
// .replace(/<\?xml[^>]*\?>/gi, '') // .replace(/<\?xml[^>]*\?>/gi, '')
// https://github.com/the-djmaze/snappymail/issues/1860
.replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
.replace(/<(\/?)head(\s[^>]*)?>/gi, '') .replace(/<(\/?)head(\s[^>]*)?>/gi, '')
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>') .replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
// .replace(/<\/?(html|head)[^>]*>/gi, '') // .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( tmpl.content.querySelectorAll(
disallowedTags ':not('+allowedTags+'),a:empty,span:empty'
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '') + (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove()); ).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 // https://github.com/the-djmaze/snappymail/issues/1641
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement)); 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 => { [...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName, const name = oElement.tagName,
@ -331,6 +391,13 @@ export const
return; return;
} }
if ('XMP' === name) {
const pre = createElement('pre');
pre.innerHTML = encodeHtml(oElement.innerHTML);
oElement.replaceWith(pre);
return;
}
// \MailSo\Base\HtmlUtils::ClearTags() // \MailSo\Base\HtmlUtils::ClearTags()
if ('none' == oStyle.display if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility || 'hidden' == oStyle.visibility
@ -417,7 +484,7 @@ export const
} }
// if (['CENTER','FORM'].includes(name)) { // 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); ('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return; return;
} }
@ -430,70 +497,73 @@ export const
*/ */
let skipStyle = false; let skipStyle = false;
value = delAttribute('src'); if (isMsg) {
if (value) { value = isMsg && delAttribute('src');
if ('IMG' === name) { if (value) {
oElement.loading = 'lazy'; if ('IMG' === name) {
let attachment; oElement.loading = 'lazy';
if (value.startsWith('cid:')) let attachment;
{ if (value.startsWith('cid:'))
value = value.slice(4); {
setAttribute('data-x-src-cid', value); setAttribute('data-x-src', value);
attachment = findAttachmentByCid(value); value = value.slice(4);
if (attachment?.download) { attachment = findAttachmentByCid(value);
oElement.src = attachment.linkPreview(); if (attachment?.download) {
oElement.title += ' ('+attachment.fileName+')'; oElement.src = attachment.linkPreview();
attachment.isInline(true); // oElement.title || (oElement.title = '+attachment.fileName);
attachment.isLinked(true); attachment.isInline(true);
attachment.isLinked(true);
}
} }
} else if ((attachment = findLocationByCid(value)))
else if ((attachment = findLocationByCid(value))) {
{ if (attachment.download) {
if (attachment.download) { oElement.src = attachment.linkPreview();
oElement.src = attachment.linkPreview(); attachment.isLinked(true);
attachment.isLinked(true); }
} }
} else if (detectHiddenImages
else if (detectHiddenImages && ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in' || (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in' || (oStyle.width && 2 > pInt(oStyle.width))
|| [ || [
'email.microsoftemail.com/open', 'email.microsoftemail.com/open',
'github.com/notifications/beacon/', 'github.com/notifications/beacon/',
'/track/open', // mandrillapp.com list-manage.com '/track/open', // mandrillapp.com list-manage.com
'google-analytics.com' 'google-analytics.com'
].filter(uri => value.toLowerCase().includes(uri)).length ].filter(uri => value.toLowerCase().includes(uri)).length
)) { )) {
skipStyle = true; skipStyle = true;
oStyle.display = 'none'; oStyle.display = 'none';
// setAttribute('style', 'display:none'); // setAttribute('style', 'display:none');
setAttribute('data-x-src-hidden', value); setAttribute('data-x-src-hidden', value);
// result.tracking = true; // result.tracking = true;
} }
else if (httpre.test(value)) else if (httpre.test(value))
{ {
let src = stripTracking(value); let src = stripTracking(value);
if (src != value) { if (src != value) {
result.tracking = true; result.tracking = true;
setAttribute('data-x-src-tracking', value); 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 else
{ {
setAttribute('data-x-src-broken', value); setAttribute('data-x-src-broken', value);
} }
} }
else
{
setAttribute('data-x-src-broken', value);
}
} }
if (hasAttribute('background')) { if (hasAttribute('background')) {
@ -556,12 +626,13 @@ export const
if (urls_broken.length) { if (urls_broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken)); setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
} }
/*
// https://github.com/the-djmaze/snappymail/issues/1082 // https://github.com/the-djmaze/snappymail/issues/1082
if (11 > pInt(oStyle.fontSize)) { if (minFontSize > pInt(oStyle.fontSize)) {
oStyle.removeProperty('font-size'); oStyle.fontSize = minFontSize + "px";
// oStyle.removeProperty('font-size');
} }
*/
// Removes background and color // Removes background and color
// Many e-mails incorrectly only define one, not both // Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability // And in dark theme mode this kills the readability
@ -579,7 +650,7 @@ export const
} }
}); });
blockquoteSwitcher(); isMsg && blockquoteSwitcher();
// return tmpl.content.firstChild; // return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim(); result.html = tmpl.innerHTML.trim();
@ -591,6 +662,9 @@ export const
* @returns {string} * @returns {string}
*/ */
htmlToPlain = html => { htmlToPlain = html => {
if (SettingsUserStore.markdown()) {
return htmlToMarkdown(html);
}
const const
hr = '⎯'.repeat(64), hr = '⎯'.repeat(64),
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn), forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
@ -611,6 +685,8 @@ export const
html = html html = html
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) => .replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '') 1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
// Remove line duplication
.replace(/<br><\/div>/gi, '</div>')
.replace(/\r?\n/g, '') .replace(/\r?\n/g, '')
.replace(/\s+/gm, ' '); .replace(/\s+/gm, ' ');
@ -625,6 +701,8 @@ export const
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t') .replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n'); .replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
forEach('style', node => node.remove());
// lines // lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`)); forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
@ -661,7 +739,9 @@ export const
forEach('a', a => { forEach('a', a => {
let txt = a.textContent, href = a.href; let txt = a.textContent, href = a.href;
return a.replaceWith( 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(); return (tmpl.content.textContent || '').trim();
}, },
htmlToMarkdown = html => {
tmpl.innerHTML = html;
return turndown.turndown(tmpl.content);
},
/** /**
* @param {string} plain * @param {string} plain
* @param {boolean} findEmailAndLinksInText = false * @param {boolean} findEmailAndLinksInText = false
@ -753,6 +838,9 @@ export const
}; };
rl.Utils = { rl.Utils = {
cleanHtml: cleanHtml,
htmlToPlain: htmlToPlain, htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml plainToHtml: plainToHtml,
htmlToMarkdown: htmlToMarkdown
// markdownToHtml: md => marked.parse(md)
}; };

View file

@ -38,14 +38,14 @@ export const
/** /**
* @param {string} download * @param {string} download
* @param {string=} customSpecSuffix
* @returns {string} * @returns {string}
*/ */
attachmentDownload = (download, customSpecSuffix) => attachmentDownload = (download) =>
serverRequestRaw('Download', download, customSpecSuffix), serverRequestRaw('Download', download),
proxy = url => proxy = url =>
BASE + '?/ProxyExternal/' 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(/=+$/, ''), + btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')), // + b64EncodeJSONSafe(url.replace(/ /g, '%20')),

View file

@ -45,8 +45,7 @@ export const
} }
} }
if (Intl.RelativeTimeFormat) { if (Intl.RelativeTimeFormat) {
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang); return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
return rtf.format(seconds, unit);
} }
// Safari < 14 // Safari < 14
abs = Math.abs(seconds); abs = Math.abs(seconds);
@ -62,7 +61,7 @@ export const
* @returns {string} * @returns {string}
*/ */
i18n = (key, valueList, defaulValue) => { i18n = (key, valueList, defaulValue) => {
let result = null == defaulValue ? key : defaulValue; let result = defaulValue ?? key;
let path = key.split('/'); let path = key.split('/');
if (I18N_DATA[path[0]] && path[1]) { if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result; 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 * @param {*} code
* @returns {string} * @returns {string}
@ -218,7 +220,6 @@ export const
}), }),
/** /**
*
* @param {string} language * @param {string} language
* @param {boolean=} isEng = false * @param {boolean=} isEng = false
* @returns {string} * @returns {string}
@ -228,6 +229,8 @@ export const
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language, 'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null, null,
language language
); ),
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
init(); init();

View file

@ -10,7 +10,7 @@ export const
pInt = (value, defaultValue = 0) => { pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10); value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value; return isFinite(value) ? value : defaultValue;
}, },
defaultOptionsAfterRender = (domItem, item) => defaultOptionsAfterRender = (domItem, item) =>

View file

@ -18,15 +18,21 @@ import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity'; import { IdentityUserStore } from 'Stores/User/Identity';
import { isArray } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { showScreenPopup } from 'Knoin/Knoin';
import { IdentityPopupView } from 'View/Popup/Identity';
export const export const
moveAction = ko.observable(false), // 1 = move, 2 = copy
moveAction = ko.observable(0),
dropdownsDetectVisibility = (() => dropdownsDetectVisibility = (() =>
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show'))) dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
).debounce(50), ).debounce(50),
editIdentity = Identity => showScreenPopup(IdentityPopupView, [Identity]),
loadAccountsAndIdentities = () => { loadAccountsAndIdentities = () => {
AccountUserStore.loading(true); AccountUserStore.loading(true);
IdentityUserStore.loading(true); IdentityUserStore.loading(true);
@ -48,6 +54,11 @@ loadAccountsAndIdentities = () => {
? items.map(identityData => IdentityModel.reviveFromJson(identityData)) ? 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, next = 0,
limit = 2; limit = 2;
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) { if (1 < pageCount) {
if (pageCount < currentPage) { if (pageCount < currentPage) {
fAdd(pageCount); fAdd(pageCount);
prev = 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)); moveAction.subscribe(value => value && leftPanelDisabled(false));

View file

@ -6,7 +6,7 @@ export class CheckboxComponent {
: ko.observable(!!params.value); : ko.observable(!!params.value);
this.enable = ko.isObservable(params.enable) ? params.enable this.enable = ko.isObservable(params.enable) ? params.enable
: ko.observable(undefined === params.enable || !!params.enable); : ko.observable(params.enable ?? 1);
this.label = params.label; this.label = params.label;
} }

View file

@ -124,7 +124,7 @@ export class JCard {
// VCardProperty argument // VCardProperty argument
else if (arg instanceof VCardProperty) { else if (arg instanceof VCardProperty) {
let propArray = this.props.get(arg.getField()); 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)); throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
propArray.splice(propArray.indexOf(arg), 1); propArray.splice(propArray.indexOf(arg), 1);
if (propArray.length === 0) if (propArray.length === 0)

View file

@ -3,10 +3,6 @@
(doc => { (doc => {
const 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, i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey() + ' + ', ctrlKey = shortcuts.getMetaKey() + ' + ',
@ -21,38 +17,17 @@ const
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn), forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
frag.append(...parent.childNodes);
return frag;
},
SquireDefaultConfig = { SquireDefaultConfig = {
/* /*
addLinks: true // allow_smart_html_links addLinks: true // allow_smart_html_links
*/ */
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
tpl.innerHTML = (html||'') html = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'') .replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(/<!--[^>]+-->/g,'') .replace(/<!--[^>]+-->/g,'')
.replace(/<span[^>]*>\s*<\/span>/gi,'') .replace(/<span[^>]*>\s*<\/span>/gi,'')
.trim(); .trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
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);
}
});
}
});
}
return tpl.content; return tpl.content;
} }
}; };
@ -109,13 +84,13 @@ class SquireUI
cmd: s => squire.setStyle({ fontFamily: s.value }) cmd: s => squire.setStyle({ fontFamily: s.value })
}, },
fontSize: { fontSize: {
select: ['11px','13px','16px','20px','24px','30px'], select: [[i18n('GLOBAL/DEFAULT'),''],'11px','13px','16px','20px','24px','30px'],
defaultValueIndex: 2, defaultValueIndex: 0,
cmd: s => squire.setStyle({ fontSize: s.value }) cmd: s => squire.setStyle({ fontSize: s.value })
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values // TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
// example: // example:
// select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'], // select: ['','xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
// defaultValueIndex: 3, // defaultValueIndex: 0,
}, },
// dir: { // dir: {
// select: [ // select: [
@ -133,11 +108,11 @@ class SquireUI
dir: { dir: {
dir_ltr: { dir_ltr: {
html: '⁋', html: '⁋',
cmd: () => squire.bidi('ltr') cmd: () => squire.setTextDirection('ltr')
}, },
dir_rtl: { dir_rtl: {
html: '¶', html: '¶',
cmd: () => squire.bidi('rtl') cmd: () => squire.setTextDirection('rtl')
} }
}, },
colors: { colors: {
@ -155,7 +130,7 @@ class SquireUI
html: 'B', html: 'B',
cmd: () => this.doAction('bold'), cmd: () => this.doAction('bold'),
key: 'B', key: 'B',
matches: 'B,STRONT' matches: 'B,STRONG'
}, },
italic: { italic: {
html: 'I', html: 'I',
@ -237,7 +212,7 @@ class SquireUI
cmd: () => { cmd: () => {
let node = squire.getSelectionClosest('IMG'), let node = squire.getSelectionClosest('IMG'),
src = prompt("Image", node?.src || "https://"); src = prompt("Image", node?.src || "https://");
src?.length ? squire.insertImage(src) : (node && squire.detach(node)); src?.length ? squire.insertImage(src) : node?.remove();
}, },
matches: 'IMG' matches: 'IMG'
}, },
@ -355,6 +330,7 @@ class SquireUI
input.append(option); input.append(option);
}); });
} else { } else {
input.add(new Option(i18n('GLOBAL/DEFAULT'), ''));
Object.entries(cfg.select).forEach(([label, options]) => { Object.entries(cfg.select).forEach(([label, options]) => {
let group = createElement('optgroup'); let group = createElement('optgroup');
group.label = label; group.label = label;
@ -363,7 +339,7 @@ class SquireUI
option.style[action] = value; option.style[action] = value;
group.append(option); group.append(option);
}); });
input.append(group); input.add(group);
}); });
} }
ev = 'input'; ev = 'input';
@ -405,9 +381,47 @@ class SquireUI
let changes = actions.changes; let changes = actions.changes;
changes.undo.input.disabled = changes.redo.input.disabled = true; changes.undo.input.disabled = changes.redo.input.disabled = true;
squire.addEventListener('undoStateChange', state => { squire.addEventListener('undoStateChange', e => {
changes.undo.input.disabled = !state.canUndo; changes.undo.input.disabled = !e.detail.canUndo;
changes.redo.input.disabled = !state.canRedo; 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('<img alt="" style="width:100%;max-width:'+width+'px" src="'+canvas.toDataURL()+'">', true);
};
img.src = event.target.result;
}
reader.readAsDataURL(item.getAsFile());
break;
}
}
}); });
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex; 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(); 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(actions, entries => {
forEachObjectValue(entries, cfg => { forEachObjectValue(entries, cfg => {
// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches)); // Check if selection has a matching parent or contains a matching element
cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot)); 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 // try to find font-family and/or font-size and set "select" elements' values
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex; let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
let familySelectedIndex = defaultFontFamilyIndex; let familySelectedIndex = defaultFontFamilyIndex;
let elm = e.element;
let familyFound = false; let familyFound = false;
let sizeFound = false; let sizeFound = false;
do { do {
@ -526,10 +547,10 @@ class SquireUI
}); });
/* /*
squire.addEventListener('cursor', e => { squire.addEventListener('cursor', e => {
console.dir({cursor:e.range}); console.dir({cursor:e.detail.range});
}); });
squire.addEventListener('select', e => { squire.addEventListener('select', e => {
console.dir({select:e.range}); console.dir({select:e.detail.range});
}); });
*/ */
} }

View file

@ -97,7 +97,7 @@ Object.assign(ko.bindingHandlers, {
fValue.subscribe(fUpdateEditorValue); fValue.subscribe(fUpdateEditorValue);
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => { // ko.addDisposeCallback(element, () => {
// }); // });
} }
} }

7
dev/External/ko.js vendored
View file

@ -15,6 +15,7 @@ export const
* based solely on the values of other observables in the application * based solely on the values of other observables in the application
*/ */
koComputable = fn => ko.computed(fn, {'pure':true}), koComputable = fn => ko.computed(fn, {'pure':true}),
// koObservable = value => ko.observable(value),
addObservablesTo = (target, observables) => addObservablesTo = (target, observables) =>
forEachObjectEntry(observables, (key, value) => forEachObjectEntry(observables, (key, value) =>
@ -30,7 +31,7 @@ export const
onEvent = (element, event, fn) => { onEvent = (element, event, fn) => {
element.addEventListener(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) => { onKey = (key, element, fValueAccessor, fAllBindings, model) => {
@ -175,6 +176,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions // functions
ko.observable.fn.askDeleteHelper = function() { ko.observable.fn.askDeleteHelper = function(prop='askDelete') {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] }); return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, prop] });
}; };

View file

@ -1,55 +0,0 @@
<html>
<head>
<meta charset="utf-8"/>
<title></title>
<style>
html, body {
margin: 0;
padding: 0;
}
header {
background: rgba(125,128,128,0.3);
border-bottom: 1px solid #888;
}
header h1 {
font-size: 120%;
}
header * {
margin: 5px 0;
}
header time {
float: right;
}
blockquote {
border-left: 2px solid rgba(125,128,128,0.5);
margin: 0;
padding: 0 0 0 10px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
word-break: normal;
}
body > * {
padding: 0.5em 1em;
}
#attachments > * {
border: 1px solid rgba(125,128,128,0.5);
padding: 0.25em;
margin-right: 1em;
}
#attachments > *::before {
content: '📎 ';
}
</style>
</head>
<body></body>
</html>

View file

@ -60,10 +60,9 @@ export class AbstractViewPopup extends AbstractView
this.keyScope.scope = name; this.keyScope.scope = name;
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 }); this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
this.close = () => this.modalVisible(false); this.close = () => this.modalVisible(false);
this.tryToClose = () => (false === this.onClose()) || this.close();
addShortcut('escape,close', '', name, () => { addShortcut('escape,close', '', name, () => {
if (this.modalVisible() && false !== this.onClose()) { this.modalVisible() && this.tryToClose();
this.close();
}
return false; return false;
// return true; Issue with supported modal close // return true; Issue with supported modal close
}); });
@ -116,6 +115,11 @@ export class AbstractViewSettings
onHide() {} onHide() {}
viewModelDom 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) addSetting(name, valueCb)
{ {
let prop = name[0].toLowerCase() + name.slice(1), let prop = name[0].toLowerCase() + name.slice(1),
@ -131,6 +135,7 @@ export class AbstractViewSettings
rl.app.Remote.saveSetting(name, value, rl.app.Remote.saveSetting(name, value,
iError => { iError => {
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success); this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
// iError || Settings.set(name, value);
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000); 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) addSettings(names)
{ {
names.forEach(name => { names.forEach(name => {

View file

@ -74,13 +74,17 @@ const
// vmDom.addEventListener('close', () => vm.modalVisible(false)); // vmDom.addEventListener('close', () => vm.modalVisible(false));
// show/hide popup/modal // 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 => { const endShowHide = e => {
if (e.target === vmDom) { if (e.target === vmDom && 'opacity' === e.propertyName) {
if (vmDom.classList.contains('animate')) { if (vmDom.classList.contains('animate')) {
vm.afterShow?.(); vm.afterShow?.();
fireEvent('rl-vm-visible', vm);
} else { } else {
vmDom.close(); vmDom.close();
vm.afterHide?.(); vm.afterHide?.();
// fireEvent('rl-vm-hidden', vm);
} }
} }
}; };
@ -163,7 +167,7 @@ const
if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) { if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) {
// Close all popups // Close all popups
for (let vm of visiblePopups) { for (let vm of visiblePopups) {
(false === vm.onClose()) || vm.close(); vm.tryToClose();
} }
let vmScreen = screen(screenName); let vmScreen = screen(screenName);

View file

@ -83,7 +83,7 @@ export function ParseMime(text)
get body() { get body() {
let body = this.bodyRaw, let body = this.bodyRaw,
charset = this.header('content-type')?.params.charset, charset = this.header('content-type')?.params.charset,
encoding = this.headerValue('content-transfer-encoding'); encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
if ('quoted-printable' == encoding) { if ('quoted-printable' == encoding) {
body = QPDecode(body); body = QPDecode(body);
} else if ('base64' == encoding) { } else if ('base64' == encoding) {
@ -94,7 +94,7 @@ export function ParseMime(text)
get dataUrl() { get dataUrl() {
let body = this.bodyRaw, let body = this.bodyRaw,
encoding = this.headerValue('content-transfer-encoding'); encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
if ('base64' == encoding) { if ('base64' == encoding) {
body = body.replace(/\r?\n/g, ''); body = body.replace(/\r?\n/g, '');
} else { } else {
@ -112,7 +112,7 @@ export function ParseMime(text)
} }
getByContentType(type) { getByContentType(type) {
if (type == this.headerValue('content-type')) { if (type == this.headerValue('content-type')?.toLowerCase()) {
return this; return this;
} }
let i = 0, p = this.parts, part; let i = 0, p = this.parts, part;

View file

@ -18,6 +18,7 @@ export function MimeToMessage(data, message)
subject = struct.headerValue('subject'); subject = struct.headerValue('subject');
html = html ? html.body : ''; html = html ? html.body : '';
// Content-Type: ...; protected-headers="v1"
subject && message.subject(subject); subject && message.subject(subject);
// EmailCollectionModel // EmailCollectionModel
@ -99,6 +100,4 @@ export function MimeToMessage(data, message)
if (message.plain().includes(BEGIN_PGP_MESSAGE)) { if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
message.pgpSigned(true); message.pgpSigned(true);
} }
// TODO: Verify instantly?
} }

View file

@ -1,3 +1,4 @@
import { baseCollator } from 'Common/Translator';
import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { AttachmentModel } from 'Model/Attachment'; import { AttachmentModel } from 'Model/Attachment';
@ -11,7 +12,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/ */
static reviveFromJson(items) { static reviveFromJson(items) {
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment)); 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) => { attachments.sort((a, b) => {
if (a.isInline()) { if (a.isInline()) {
if (!b.isInline()) { if (!b.isInline()) {

View file

@ -245,7 +245,7 @@ export class ContactModel extends AbstractModel {
this.namePrefix(), this.namePrefix(),
this.nameSuffix() this.nameSuffix()
]/*, params, group*/); ]/*, params, group*/);
// jCard.parseFullName({set:true}); jCard.parseFullName({set:true});
['nickname', 'note', 'title'].forEach(field => ['nickname', 'note', 'title'].forEach(field =>
this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field) this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field)

View file

@ -73,4 +73,9 @@ export class EmailModel extends AbstractModel {
} }
return result || name; return result || name;
} }
get domain() {
return this.email.replace(/^.+@([^@]+)$/, '$1');
// return this.email.slice(this.email.lastIndexOf('@'))
}
} }

View file

@ -29,6 +29,8 @@ import { FileInfo } from 'Common/File';
import { FolderPopupView } from 'View/Popup/Folder'; import { FolderPopupView } from 'View/Popup/Folder';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { isAllowedKeyword } from 'Stores/User/Folder';
const const
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()), // isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
@ -102,7 +104,7 @@ export const
// Repeat every 15 minutes? // Repeat every 15 minutes?
// this.foldersTimeout = setTimeout(loadFolders, 900000); // this.foldersTimeout = setTimeout(loadFolders, 900000);
}) })
.catch(() => fCallback && setTimeout(fCallback, 1, false)); .catch(e => fCallback && setTimeout(fCallback, 1, false, e));
}; };
export class FolderCollectionModel extends AbstractCollectionModel export class FolderCollectionModel extends AbstractCollectionModel
@ -411,6 +413,8 @@ export class FolderModel extends AbstractModel {
canBeSubscribed: () => this.selectable() canBeSubscribed: () => this.selectable()
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()), && !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
/** /**
* Folder is visible when: * Folder is visible when:
* - hasVisibleSubfolders() * - hasVisibleSubfolders()
@ -467,16 +471,25 @@ export class FolderModel extends AbstractModel {
detailedName: () => this.name() + ' ' + this.nameInfo(), detailedName: () => this.name() + ' ' + this.nameInfo(),
hasSubscribedUnreadMessagesSubfolders: () => icon: () => {
!!this.subFolders().find( switch (this.type())
folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders() {
) case 1: return '📥'; // FolderType.Inbox
/* case 2: return '📧'; // FolderType.Sent icon-paper-plane
!!this.subFolders().filter( case 3: return '🗎'; // FolderType.Drafts
folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders() case 4: return '⚠'; // FolderType.Junk
).length case 5: return '🗑'; // FolderType.Trash
*/ case 6: return '🗄'; // FolderType.Archive
,href: () => this.canBeSelected() && mailBox(this.fullNameHash) }
return null;
},
hasUnreadInSub: () =>
this.subFolders().some(
folder => folder.unreadEmails() | folder.hasUnreadInSub()
),
href: () => this.canBeSelected() && mailBox(this.fullNameHash)
}); });
} }

View file

@ -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'; import { addObservablesTo, addComputablesTo } from 'External/ko';
export class IdentityModel extends AbstractModel { export class IdentityModel extends EmailModel /*AbstractModel*/ {
/**
* @param {string} id
* @param {string} email
*/
constructor() { constructor() {
super(); super();
addObservablesTo(this, { addObservablesTo(this, {
id: '', id: '',
label: '', label: '',
email: '', // email: '',
name: '', // name: '',
replyTo: '', replyTo: '',
bcc: '', bcc: '',
@ -27,23 +24,28 @@ export class IdentityModel extends AbstractModel {
smimeKey: '', smimeKey: '',
smimeCertificate: '', smimeCertificate: '',
smimeCertificateChain: '',
askDelete: false askDelete: false,
exists: false
}); });
addComputablesTo(this, { addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'), smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()), smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()) smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()),
smimeCertificateChainValid: () => !this.smimeCertificateChain()
|| /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificateChain())
}); });
} }
/** /**
* @returns {string} * @returns {string}
*/ */
formattedName() { toString() {
const name = this.name(), const name = this.name,
email = this.email(), email = this.email,
label = this.label(); label = this.label();
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : ''); return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
} }

View file

@ -8,7 +8,7 @@ import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { serverRequestRaw, proxy } from 'Common/Links'; import { serverRequestRaw, proxy } from 'Common/Links';
import { addObservablesTo, addComputablesTo } from 'External/ko'; 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 { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo, RFC822 } from 'Common/File'; import { FileInfo, RFC822 } from 'Common/File';
@ -18,15 +18,73 @@ import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection';
//import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt'; //import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { MimeToMessage } from 'Mime/Utils'; import { MimeToMessage } from 'Mime/Utils';
import { PgpUserStore } from 'Stores/User/Pgp';
import { IdentityUserStore } from 'Stores/User/Identity';
import { Passphrases } from 'Storage/Passphrases';
const const
PreviewHTML = `<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
html, body {
margin: 0;
padding: 0;
}
header {
background: rgba(125,128,128,0.3);
border-bottom: 1px solid #888;
}
header h1 {
font-size: 120%;
}
header * {
margin: 5px 0;
}
header time {
float: right;
}
blockquote {
border-left: 2px solid rgba(125,128,128,0.5);
margin: 0;
padding: 0 0 0 10px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
word-break: normal;
}
body > * {
padding: 0.5em 1em;
}
#attachments > * {
border: 1px solid rgba(125,128,128,0.5);
padding: 0.25em;
margin-right: 1em;
}
#attachments > *::before {
content: '📎 ';
}
</style>
</head>
<body></body>
</html>`,
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash), msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
toggleTag = (message, keyword) => { toggleTag = (message, keyword) => {
@ -81,7 +139,6 @@ export class MessageModel extends AbstractModel {
// autocrypt: ko.observableArray(), // autocrypt: ko.observableArray(),
hasVirus: null, // or boolean when scanned hasVirus: null, // or boolean when scanned
priority: 3, // Normal priority: 3, // Normal
internalTimestamp: 0,
senderEmailsString: '', senderEmailsString: '',
senderClearEmailsString: '', senderClearEmailsString: '',
isSpam: false, isSpam: false,
@ -104,8 +161,7 @@ export class MessageModel extends AbstractModel {
plain: '', plain: '',
html: '', html: '',
dateTimestamp: 0, dateTimestamp: 0,
dateTimestampSource: 0,
deleted: false,
// Also used by Selector // Also used by Selector
focused: false, focused: false,
@ -152,22 +208,21 @@ export class MessageModel extends AbstractModel {
isUnseen: () => !this.flags().includes('\\seen'), isUnseen: () => !this.flags().includes('\\seen'),
isFlagged: () => this.flags().includes('\\flagged'), isFlagged: () => this.flags().includes('\\flagged'),
isDeleted: () => this.flags().includes('\\deleted'),
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'), // isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing'), // isPhishing: () => this.flags().includes('$phishing'),
tagOptions: () => { tagOptions: () => {
const tagOptions = []; const tagOptions = [];
FolderUserStore.currentFolder().permanentFlags.forEach(value => { FolderUserStore.currentFolder().optionalTags().forEach(value => {
if (isAllowedKeyword(value)) { let lower = value.toLowerCase();
let lower = value.toLowerCase(); tagOptions.push({
tagOptions.push({ css: 'msgflag-' + lower,
css: 'msgflag-' + lower, value: value,
value: value, checked: this.flags().includes(lower),
checked: this.flags().includes(lower), label: i18n('MESSAGE_TAGS/'+lower, 0, value),
label: i18n('MESSAGE_TAGS/'+lower, 0, value), toggle: (/*obj*/) => toggleTag(this, value)
toggle: (/*obj*/) => toggleTag(this, value) });
});
}
}); });
return tagOptions return tagOptions
}, },
@ -311,7 +366,6 @@ export class MessageModel extends AbstractModel {
lineAsCss(flags=1) { lineAsCss(flags=1) {
let classes = []; let classes = [];
forEachObjectEntry({ forEachObjectEntry({
deleted: this.deleted(),
selected: this.selected(), selected: this.selected(),
checked: this.checked(), checked: this.checked(),
unseen: this.isUnseen(), unseen: this.isUnseen(),
@ -513,7 +567,7 @@ export class MessageModel extends AbstractModel {
hasImages = true; hasImages = true;
}, },
attr = 'data-x-src', attr = 'data-x-src',
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages'); src, useProxy = !!SettingsGet('proxyExternalImages');
body.querySelectorAll('img[' + attr + ']').forEach(node => { body.querySelectorAll('img[' + attr + ']').forEach(node => {
src = node.getAttribute(attr); src = node.getAttribute(attr);
if (isValid(src)) { if (isValid(src)) {
@ -548,4 +602,140 @@ export class MessageModel extends AbstractModel {
return result.html || plainToHtml(this.plain()); 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);
}
});
}
}
} }

View file

@ -30,12 +30,13 @@ export class MessageCollectionModel extends AbstractCollectionModel
let msg = MessageUserStore.message(); let msg = MessageUserStore.message();
return super.reviveFromJson(object, message => { return super.reviveFromJson(object, message => {
// If message is currently viewed, use that. // If message is currently viewed, use that.
// Maybe then use msg.revivePropertiesFromJson(message) ? if (msg && msg.hash === message.hash) {
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message); msg.revivePropertiesFromJson(message);
if (message) { message = msg;
message.deleted(false); } else {
return message; message = MessageModel.reviveFromJson(message);
} }
return message;
}); });
} }
} }

View file

@ -10,18 +10,20 @@ export class MimeHeaderAutocryptModel/* extends AbstractModel*/
if (value) { if (value) {
value.split(';').forEach(entry => { value.split(';').forEach(entry => {
entry = entry.split('='); entry = entry.match(/^([^=]+)=(.*)$/);
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, ''); 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() { toString() {
let result = `addr=${this.addr}; `;
if ('mutual' === this.prefer_encrypt) { 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() { pem() {

View file

@ -8,11 +8,11 @@ let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + pString(add), const getURL = (add = '') => serverRequest('Json') + pString(add),
checkResponseError = data => { checkResponseError = data => {
const err = data ? data.ErrorCode : null; const err = data ? data.code : null;
if (Notifications.InvalidToken === err) { if (Notifications.InvalidToken === err) {
console.error(getNotification(err)); console.error(getNotification(err) + ` (${data.messageAdditional})`);
// alert(getNotification(err)); // alert(getNotification(err));
rl.logoutReload(); setTimeout(rl.logoutReload, 5000);
} else if ([ } else if ([
Notifications.AuthError, Notifications.AuthError,
Notifications.ConnectionError, Notifications.ConnectionError,
@ -131,7 +131,7 @@ export class AbstractFetchRemote
fetchJSON(sAction, getURL(sGetAdd), fetchJSON(sAction, getURL(sGetAdd),
sGetAdd ? null : (params || {}), sGetAdd ? null : (params || {}),
undefined === iTimeout ? 30000 : pInt(iTimeout), pInt(iTimeout ?? 30000),
async data => { async data => {
let iError = 0; let iError = 0;
if (data) { if (data) {
@ -144,7 +144,7 @@ export class AbstractFetchRemote
iJsonErrorCount = 0; iJsonErrorCount = 0;
} else { } else {
checkResponseError(data); 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)); 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); return this.post(action, fTrigger, params, timeOut);
} }
/* /*
@ -222,8 +222,8 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) { if (!data.Result || action !== data.Action) {
checkResponseError(data); checkResponseError(data);
return Promise.reject(new FetchError( return Promise.reject(new FetchError(
data ? data.ErrorCode : 0, data ? data.code : 0,
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : '' data ? (data.messageAdditional || data.message) : ''
)); ));
} }

View file

@ -117,7 +117,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
rules = { rules = {
subname: /^(.*)$/, subname: /^(.*)$/,
normalize_: (rquest, vals) => { normalize_: (rquest, vals) => {
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname); vals.subname = pString(vals.subname ?? defaultRoute);
return [vals.subname]; return [vals.subname];
} }
}; };

View file

@ -109,7 +109,7 @@ export class MailBoxUserScreen extends AbstractScreen {
*/ */
onBuild() { onBuild() {
doc.addEventListener('click', event => doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false) event.target.closest('#rl-right') && moveAction(0)
); );
} }

View file

@ -16,6 +16,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
coreWarning: false, coreWarning: false,
coreVersion: '', coreVersion: '',
coreVersionCompare: -2, coreVersionCompare: -2,
php64: true,
load1: 0, load1: 0,
load5: 0, load5: 0,
load15: 0, load15: 0,
@ -69,6 +70,7 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
this.coreWarning(!!data.core.warning); this.coreWarning(!!data.core.warning);
this.coreVersion(data.core.version || ''); this.coreVersion(data.core.version || '');
this.coreVersionCompare(data.core.versionCompare); this.coreVersionCompare(data.core.versionCompare);
this.php64(data.php[1].loaded);
} else { } else {
this.coreReal(false); this.coreReal(false);
this.coreWarning(false); this.coreWarning(false);
@ -78,6 +80,10 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
}); });
} }
clearCache() {
Remote.request('AdminClearCache');
}
updateCoreData() { updateCoreData() {
if (!this.coreUpdating()) { if (!this.coreUpdating()) {
this.coreUpdating(true); this.coreUpdating(true);

View file

@ -10,7 +10,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.config = ko.observableArray(); this.config = ko.observableArray();
this.search = ko.observableArray(); this.search = ko.observable('');
this.saved = ko.observable(false).extend({ falseTimeout: 5000 }); this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
this.search.subscribe(value => { this.search.subscribe(value => {

View file

@ -18,8 +18,8 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
testUsername() { testUsername() {
Remote.request('AdminDomainMatch', Remote.request('AdminDomainMatch',
(iError, oData) => { (iError, oData) => {
if (oData?.Result?.domain) { if (oData?.Result?.name) {
alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`); alert(`${oData.Result.email} matched domain: ${oData.Result.name}`);
} else { } else {
alert('No domain match'); alert('No domain match');
} }

View file

@ -18,7 +18,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
this.addSettings(['pluginsEnable']); this.addSettings(['pluginsEnable']);
addObservablesTo(this, { addObservablesTo(this, {
packagesError: '' packagesError: '',
search: ''
}); });
this.packages = PackageAdminStore; this.packages = PackageAdminStore;
@ -30,6 +31,19 @@ export class AdminSettingsPackages extends AbstractViewSettings {
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden') 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() { onShow() {
@ -68,7 +82,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) { if (iError) {
this.packagesError( this.packagesError(
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage) getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '') + (data.message ? ':\n' + data.message : '')
); );
} else if (data.Result.Reload) { } else if (data.Result.Reload) {
location.reload(); location.reload();
@ -113,8 +127,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
if (iError) { if (iError) {
plugin.enabled(disable); plugin.enabled(disable);
this.packagesError( this.packagesError(
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage) (Notifications.UnsupportedPluginPackage === iError && data?.message)
? data.ErrorMessage ? data.message
: getNotification(iError) : getNotification(iError)
); );
} }

View file

@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']); this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);
this.weakPassword = rl.app.weakPassword; this.weakPassword = rl.app.weakPassword;
@ -32,6 +32,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
capaOpenPGP: SettingsCapa('OpenPGP') capaOpenPGP: SettingsCapa('OpenPGP')
}); });
this.gnuPGversion = 'GnuPG v' + SettingsGet('gnupg');
const reset = () => { const reset = () => {
this.saveError(false); this.saveError(false);
this.saveSuccess(false); this.saveSuccess(false);

View file

@ -2,7 +2,7 @@ import ko from 'ko';
//import { koComputable } from 'External/ko'; //import { koComputable } from 'External/ko';
import { SettingsCapa, SettingsGet } from 'Common/Globals'; import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { loadAccountsAndIdentities } from 'Common/UtilsUser'; import { loadAccountsAndIdentities, editIdentity } from 'Common/UtilsUser';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity'; import { IdentityUserStore } from 'Stores/User/Identity';
@ -12,7 +12,6 @@ import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account'; import { AccountPopupView } from 'View/Popup/Account';
import { IdentityPopupView } from 'View/Popup/Identity';
export class UserSettingsAccounts /*extends AbstractViewSettings*/ { export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() { constructor() {
@ -44,11 +43,11 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
} }
addNewIdentity() { addNewIdentity() {
showScreenPopup(IdentityPopupView); editIdentity();
} }
editIdentity(identity) { editIdentity(identity) {
showScreenPopup(IdentityPopupView, [identity]); editIdentity(identity);
} }
/** /**
@ -89,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
accountsAndIdentitiesAfterMove() { accountsAndIdentitiesAfterMove() {
Remote.request('AccountsAndIdentitiesSortOrder', null, { 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() : "")) Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
}); });
} }

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { koComputable } from 'External/ko'; import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals'; 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 { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
@ -15,6 +15,7 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
this.syncUrl = ContactUserStore.syncUrl; this.syncUrl = ContactUserStore.syncUrl;
this.syncUser = ContactUserStore.syncUser; this.syncUser = ContactUserStore.syncUser;
this.syncPass = ContactUserStore.syncPass; this.syncPass = ContactUserStore.syncPass;
this.syncError = ko.observable('');
this.syncModeOptions = koComputable(() => { this.syncModeOptions = koComputable(() => {
translateTrigger(); 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()
})
}
} }

View file

@ -6,15 +6,16 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { setRefreshFoldersInterval } from 'Common/Folders'; import { setRefreshFoldersInterval } from 'Common/Folders';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings, SettingsGet } from 'Common/Globals';
import { WYSIWYGS } from 'Common/HtmlEditor'; import { WYSIWYGS } from 'Common/HtmlEditor';
import { isArray } from 'Common/Utils';
import { addSubscribablesTo, addComputablesTo } from 'External/ko'; import { addSubscribablesTo, addComputablesTo } from 'External/ko';
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator'; import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
import { editIdentity } from 'Common/UtilsUser';
import { AbstractViewSettings } from 'Knoin/AbstractViews'; import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity'; import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification'; import { NotificationUserStore } from 'Stores/User/Notification';
@ -22,13 +23,14 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages'; import { LanguagesPopupView } from 'View/Popup/Languages';
export class UserSettingsGeneral extends AbstractViewSettings { export class UserSettingsGeneral extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
this.mailto = ko.observable(!!navigator.registerProtocolHandler);
this.language = LanguageStore.language; this.language = LanguageStore.language;
this.languages = LanguageStore.languages; this.languages = LanguageStore.languages;
this.hourCycle = LanguageStore.hourCycle; this.hourCycle = LanguageStore.hourCycle;
@ -37,21 +39,74 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.notificationSound = ko.observable(SettingsGet('NotificationSound')); this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds')); this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
this.minRefreshInterval = SettingsGet('minRefreshInterval');
this.desktopNotifications = NotificationUserStore.enabled; this.desktopNotifications = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed; 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() // These use addSetting()
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', 'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'minFontSize',
// These are in addSettings() // These are in addSettings()
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck', 'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
'messageReadAuto', 'showNextMessage', 'messageNewWindow' 'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown'
].forEach(name => this[name] = SettingsUserStore[name]); ].forEach(name => this[name] = SettingsUserStore[name]);
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings'); this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
@ -65,14 +120,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
addComputablesTo(this, { addComputablesTo(this, {
languageFullName: () => convertLangName(this.language()), languageFullName: () => convertLangName(this.language()),
identityMain: () => {
const list = this.identities();
return isArray(list) ? list.find(item => item && !item.id()) : null;
},
identityMainDesc: () => { identityMainDesc: () => {
const identity = this.identityMain(); const identity = IdentityUserStore.main();
return identity ? identity.formattedName() : '---'; return identity ? identity.toString() : '---';
}, },
editorDefaultTypes: () => { editorDefaultTypes: () => {
@ -111,13 +161,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('CheckMailInterval'); this.addSetting('CheckMailInterval');
this.addSetting('Layout'); this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel'); this.addSetting('MaxBlockquotesLevel');
this.addSetting('minFontSize');
this.addSetting('defaultSort');
this.addSettings([ this.addSettings([
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave', 'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes', 'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown',
'DesktopNotifications', 'SoundNotification']); 'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => { const fReloadLanguageHelper = (saveSettingsStep) => () => {
@ -146,6 +198,11 @@ export class UserSettingsGeneral extends AbstractViewSettings {
Remote.saveSetting('UseThreads', value); Remote.saveSetting('UseThreads', value);
}, },
threadAlgorithm: value => {
MessagelistUserStore([]);
Remote.saveSetting('threadAlgorithm', value);
},
checkMailInterval: () => { checkMailInterval: () => {
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval()); setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
} }
@ -153,8 +210,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
} }
editMainIdentity() { editMainIdentity() {
const identity = this.identityMain(); editIdentity(IdentityUserStore.main());
identity && showScreenPopup(IdentityPopupView, [identity]);
} }
testSoundNotification() { testSoundNotification() {
@ -168,4 +224,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
selectLanguage() { selectLanguage() {
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]); 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);
}
} }

View file

@ -18,8 +18,6 @@ import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
import { SMimeUserStore } from 'Stores/User/SMime'; import { SMimeUserStore } from 'Stores/User/SMime';
import { SMimeImportPopupView } from 'View/Popup/SMimeImport'; import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
import Remote from 'Remote/User/Fetch';
export class UserSettingsSecurity extends AbstractViewSettings { export class UserSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
super(); super();
@ -28,9 +26,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
this.autoLogoutOptions = koComputable(() => { this.autoLogoutOptions = koComputable(() => {
translateTrigger(); translateTrigger();
return [ return [
{ id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') }, { id: 0, name: i18n('SETTINGS_SECURITY/NEVER') },
{ id: 5, name: relativeTime(300) }, { id: 5, name: relativeTime(300) },
{ id: 10, name: relativeTime(600) }, { id: 15, name: relativeTime(900) },
{ id: 30, name: relativeTime(1800) }, { id: 30, name: relativeTime(1800) },
{ id: 60, name: relativeTime(3600) }, { id: 60, name: relativeTime(3600) },
{ id: 120, name: relativeTime(7200) }, { id: 120, name: relativeTime(7200) },
@ -40,6 +38,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
}); });
this.addSetting('AutoLogout'); this.addSetting('AutoLogout');
this.keyPassForget = SettingsUserStore.keyPassForget;
this.addSetting('keyPassForget');
this.gnupgPublicKeys = GnuPGUserStore.publicKeys; this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys; this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
@ -62,9 +63,7 @@ export class UserSettingsSecurity extends AbstractViewSettings {
} }
importToOpenPGP() { importToOpenPGP() {
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys', OpenPGPUserStore.loadBackupKeys();
(iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
);
} }
importToSMime() { importToSMime() {

View file

@ -99,8 +99,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
themeBackground.hash(data?.Result?.hash || ''); themeBackground.hash(data?.Result?.hash || '');
if (!themeBackground.name() || !themeBackground.hash()) { if (!themeBackground.name() || !themeBackground.hash()) {
let errorMsg = ''; let errorMsg = '';
if (data.ErrorCode) { if (data.code) {
switch (data.ErrorCode) { switch (data.code) {
case UploadErrorCode.FileIsTooBig: case UploadErrorCode.FileIsTooBig:
errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG'); errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG');
break; 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'));
} }
}); });
} }

View file

@ -15,7 +15,6 @@ import {
} from 'Sieve/Commands/Actions'; } from 'Sieve/Commands/Actions';
import { import {
ConditionalCommand,
ElsIfCommand, ElsIfCommand,
ElseCommand, ElseCommand,
IfCommand, IfCommand,
@ -49,6 +48,7 @@ import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463'; import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490'; import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703'; import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703';
import { ValidExtListTest } from 'Sieve/Extensions/rfc6134';
import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609'; import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609';
export const export const
@ -67,7 +67,6 @@ export const
IfCommand, IfCommand,
ElsIfCommand, ElsIfCommand,
ElseCommand, ElseCommand,
ConditionalCommand,
RequireCommand, RequireCommand,
StopCommand, StopCommand,
// Action commands // Action commands
@ -129,6 +128,8 @@ export const
ReplaceCommand, ReplaceCommand,
EncloseCommand, EncloseCommand,
ExtractTextCommand, ExtractTextCommand,
// rfc6134
ValidExtListTest,
// rfc6609 // rfc6609
IncludeCommand, IncludeCommand,
ReturnCommand, ReturnCommand,
@ -146,6 +147,17 @@ export const
return commands; 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 = () => { availableActions = () => {
let actions = {}, id; let actions = {}, id;
AllCommands.forEach(cmd => { AllCommands.forEach(cmd => {

View file

@ -21,6 +21,10 @@ export class FileIntoCommand extends ActionCommand
super(); super();
// QuotedString / MultiLine // QuotedString / MultiLine
this._mailbox = new GrammarQuotedString(); 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'; } get require() { return 'fileinto'; }
@ -28,9 +32,7 @@ export class FileIntoCommand extends ActionCommand
toString() toString()
{ {
return 'fileinto' return 'fileinto'
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '') + ((this.copy && capa.includes('copy')) ? ' :copy' : '')
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
+ ((this.create && capa.includes('mailbox')) ? ' :create' : '') + ((this.create && capa.includes('mailbox')) ? ' :create' : '')
+ ' ' + this._mailbox + ' ' + this._mailbox
+ ';'; + ';';
@ -64,15 +66,17 @@ export class RedirectCommand extends ActionCommand
super(); super();
// QuotedString / MultiLine // QuotedString / MultiLine
this._address = new GrammarQuotedString(); 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() toString()
{ {
return 'redirect' return 'redirect'
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
// + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '') // + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '')
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '') + ((this.copy && capa.includes('copy')) ? ' :copy' : '')
+ ' ' + this._address + ' ' + this._address
+ ';'; + ';';

View file

@ -5,6 +5,7 @@
import { import {
ControlCommand, ControlCommand,
GrammarCommands,
GrammarStringList, GrammarStringList,
GrammarQuotedString GrammarQuotedString
} from 'Sieve/Grammar'; } from 'Sieve/Grammar';
@ -16,37 +17,55 @@ import {
* elsif <test2: test> <block2: block> * elsif <test2: test> <block2: block>
* else <block3: block> * else <block3: block>
*/ */
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 if (this.constructor == ConditionalCommand) {
{ throw Error("Abstract class can't be instantiated.");
args.forEach((arg, i) => { }
if (i && ':' === args[i-1][0]) {
this[args[i-1].replace(':','_')].value = arg.value;
}
});
print_r($args);
exit;
}
*/ */
super();
this.commands = new GrammarCommands;
}
} }
export class IfCommand extends ConditionalCommand 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
{ {
} }

View file

@ -40,8 +40,8 @@ export class AddressTest extends TestCommand
this.header_list = new GrammarStringList; this.header_list = new GrammarStringList;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
// rfc5260#section-6 // rfc5260#section-6
// this.index = new GrammarNumber; this.index = new GrammarNumber('');
// this.last = false; this.last = false;
// rfc5703#section-6 // rfc5703#section-6
// this.mime // this.mime
// this.anychild // this.anychild
@ -67,10 +67,11 @@ export class AddressTest extends TestCommand
} }
} }
return result return result
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.address_part + ' ' + this.address_part
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.header_list + ' ' + this.header_list
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
@ -147,9 +148,10 @@ export class EnvelopeTest extends TestCommand
toString() toString()
{ {
return 'envelope' return 'envelope'
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.address_part + ' ' + this.address_part
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.envelope_part + ' ' + this.envelope_part
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
@ -234,8 +236,8 @@ export class HeaderTest extends TestCommand
this.header_names = new GrammarStringList; this.header_names = new GrammarStringList;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
// rfc5260#section-6 // rfc5260#section-6
// this.index = new GrammarNumber; this.index = new GrammarNumber('');
// this.last = false; this.last = false;
// rfc5703#section-6 // rfc5703#section-6
this.mime = false; this.mime = false;
this.anychild = false; this.anychild = false;
@ -278,9 +280,10 @@ export class HeaderTest extends TestCommand
} }
} }
return result return result
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.header_names + ' ' + this.header_names
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
@ -316,11 +319,6 @@ export class NotTest extends TestCommand
{ {
return 'not ' + this.test; return 'not ' + this.test;
} }
pushArguments()
{
throw 'No arguments';
}
} }
/** /**

View file

@ -22,8 +22,9 @@ export class BodyTest extends TestCommand
toString() toString()
{ {
return 'body' return 'body'
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.body_transform + ' ' + this.body_transform
+ ' ' + this.key_list; + ' ' + this.key_list;
} }

View file

@ -13,24 +13,28 @@ export class EnvironmentTest extends TestCommand
constructor() constructor()
{ {
super(); super();
this.name = new GrammarQuotedString; this._name = new GrammarQuotedString;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
} }
get name() { return this._name.value; }
set name(v) { this._name.value = v; }
get require() { return 'environment'; } get require() { return 'environment'; }
toString() toString()
{ {
return 'environment' return 'environment'
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ ' ' + this.name + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._name
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
this.key_list = args.pop(); this.key_list = args.pop();
this.name = args.pop(); this._name = args.pop();
} }
} }

View file

@ -21,6 +21,12 @@ export class SetCommand extends ActionCommand
get require() { return 'variables'; } 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() toString()
{ {
return 'set' return 'set'
@ -29,12 +35,6 @@ export class SetCommand extends ActionCommand
+ ' ' + this._value; + ' ' + 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) pushArguments(args)
{ {
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => { [':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
@ -57,8 +57,9 @@ export class StringTest extends TestCommand
toString() toString()
{ {
return 'string' return 'string'
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this.relational_match ? ' ' + this.relational_match : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.source + ' ' + this.source
+ ' ' + this.key_list; + ' ' + this.key_list;
} }

View file

@ -30,33 +30,6 @@ export class VacationCommand extends ActionCommand
// get require() { return ['vacation','vacation-seconds']; } // get require() { return ['vacation','vacation-seconds']; }
get require() { return 'vacation'; } 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 days() { return this._days.value; }
get seconds() { return this._seconds.value; } get seconds() { return this._seconds.value; }
get subject() { return this._subject.value; } get subject() { return this._subject.value; }
@ -71,6 +44,32 @@ export class VacationCommand extends ActionCommand
set handle(str) { this._handle.value = str; } set handle(str) { this._handle.value = str; }
set reason(str) { this._reason.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) pushArguments(args)
{ {
this._reason.value = args.pop().value; // GrammarQuotedString this._reason.value = args.pop().value; // GrammarQuotedString
@ -81,7 +80,8 @@ export class VacationCommand extends ActionCommand
this.addresses = arg; // GrammarStringList this.addresses = arg; // GrammarStringList
} else if (i && ':' === args[i-1][0]) { } else if (i && ':' === args[i-1][0]) {
// :days, :seconds, :subject, :from, :handle // :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);
} }
}); });
} }

View file

@ -43,11 +43,12 @@ class FlagCommand extends ActionCommand
if (args[0] instanceof GrammarQuotedString) { if (args[0] instanceof GrammarQuotedString) {
this._variablename = args[0]; this._variablename = args[0];
} }
if (args[1] instanceof GrammarString) { args[0] = args[1];
this.list_of_flags = args[1]; }
} if (args[0] instanceof GrammarStringList) {
} else if (args[0] instanceof GrammarString) {
this.list_of_flags = args[0]; 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() toString()
{ {
return 'hasflag' return 'hasflag'
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this.relational_match ? ' ' + this.relational_match : '')
+ (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.variable_list + ' ' + this.variable_list
+ ' ' + this.list_of_flags; + ' ' + this.list_of_flags;
} }

View file

@ -4,7 +4,6 @@
import { import {
GrammarQuotedString, GrammarQuotedString,
GrammarString,
TestCommand TestCommand
} from 'Sieve/Grammar'; } from 'Sieve/Grammar';
@ -14,19 +13,23 @@ export class SpamTestTest extends TestCommand
{ {
super(); super();
this.percent = false, // 0 - 100 else 0 - 10 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 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() toString()
{ {
return 'spamtest' return 'spamtest'
+ (this.percent ? ' :percent' : '') + (this.percent ? ' :percent' : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ ' ' + this.value; + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._value;
} }
pushArguments(args) pushArguments(args)
@ -34,8 +37,8 @@ export class SpamTestTest extends TestCommand
args.forEach(arg => { args.forEach(arg => {
if (':percent' === arg) { if (':percent' === arg) {
this.percent = true; this.percent = true;
} else if (arg instanceof GrammarString) { } else if (arg instanceof GrammarQuotedString) {
this.value = arg; this._value = arg;
} }
}); });
} }
@ -46,24 +49,28 @@ export class VirusTestTest extends TestCommand
constructor() constructor()
{ {
super(); 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() toString()
{ {
return 'virustest' return 'virustest'
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ ' ' + this.value; + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._value;
} }
pushArguments(args) pushArguments(args)
{ {
args.forEach(arg => { args.forEach(arg => {
if (arg instanceof GrammarString) { if (arg instanceof GrammarQuotedString) {
this.value = arg; this._value = arg;
} }
}); });
} }

View file

@ -14,10 +14,10 @@ export class DateTest extends TestCommand
constructor() constructor()
{ {
super(); super();
this.zone = new GrammarQuotedString; this._zone = new GrammarQuotedString;
this.originalzone = false; this.originalzone = false;
this.header_name = new GrammarQuotedString; this._header_name = new GrammarQuotedString;
this.date_part = new GrammarQuotedString; this._date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
// rfc5260#section-6 // rfc5260#section-6
this.index = new GrammarNumber; this.index = new GrammarNumber;
@ -27,30 +27,40 @@ export class DateTest extends TestCommand
// get require() { return ['date','index']; } // get require() { return ['date','index']; }
get require() { return 'date'; } 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() toString()
{ {
return 'date' return 'date'
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : '')) + (this.originalzone ? ' :originalzone' : (this._zone.length ? ' :zone ' + this._zone : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ ' ' + this.header_name + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.date_part + ' ' + this._header_name
+ ' ' + this._date_part
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
this.key_list = args.pop(); this.key_list = args.pop();
this.date_part = args.pop(); this._date_part = args.pop();
this.header_name = args.pop(); this._header_name = args.pop();
args.forEach((arg, i) => { args.forEach((arg, i) => {
if (':originalzone' === arg) { if (':originalzone' === arg) {
this.originalzone = true; this.originalzone = true;
} else if (':last' === arg) { } else if (':last' === arg) {
this.last = true; this.last = true;
} else if (i && ':zone' === args[i-1]) { } else if (i && ':zone' === args[i-1]) {
this.zone.value = arg.value; this._zone.value = arg.value;
} else if (i && ':index' === args[i-1]) { } else if (i && ':index' === args[i-1]) {
this.index.value = arg.value; this.index.value = arg.value;
} }
@ -63,30 +73,37 @@ export class CurrentDateTest extends TestCommand
constructor() constructor()
{ {
super(); super();
this.zone = new GrammarQuotedString; this._zone = new GrammarQuotedString;
this.date_part = new GrammarQuotedString; this._date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
} }
get require() { return 'date'; } 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() toString()
{ {
return 'currentdate' return 'currentdate'
+ (this.zone.length ? ' :zone ' + this.zone : '') + (this._zone.length ? ' :zone ' + this._zone : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.match_type + (this._match_type ? ' ' + this._match_type : '')
+ ' ' + this.date_part + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this._date_part
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
this.key_list = args.pop(); this.key_list = args.pop();
this.date_part = args.pop(); this._date_part = args.pop();
args.forEach((arg, i) => { args.forEach((arg, i) => {
if (i && ':zone' === args[i-1]) { if (i && ':zone' === args[i-1]) {
this.zone.value = arg.value; this._zone.value = arg.value;
} }
}); });
} }

View file

@ -16,24 +16,30 @@ export class AddHeaderCommand extends ActionCommand
{ {
super(); super();
this.last = false; this.last = false;
this.field_name = new GrammarQuotedString; this._field_name = new GrammarQuotedString;
this.value = new GrammarQuotedString; this._value = new GrammarQuotedString;
} }
get require() { return 'editheader'; } 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() toString()
{ {
return this.identifier return this.identifier
+ (this.last ? ' :last' : '') + (this.last ? ' :last' : '')
+ ' ' + this.field_name + ' ' + this._field_name
+ ' ' + this.value + ';'; + ' ' + this._value + ';';
} }
pushArguments(args) pushArguments(args)
{ {
this.value = args.pop(); this._value = args.pop();
this.field_name = args.pop(); this._field_name = args.pop();
this.last = args.includes(':last'); this.last = args.includes(':last');
} }
} }
@ -47,19 +53,22 @@ export class DeleteHeaderCommand extends ActionCommand
this.last = false; this.last = false;
this.comparator = '', this.comparator = '',
this.match_type = ':is', this.match_type = ':is',
this.field_name = new GrammarQuotedString; this._field_name = new GrammarQuotedString;
this.value_patterns = new GrammarStringList; this.value_patterns = new GrammarStringList;
} }
get require() { return 'editheader'; } get require() { return 'editheader'; }
get field_name() { return this._field_name.value; }
set field_name(v) { this._field_name.value = v; }
toString() toString()
{ {
return this.identifier return this.identifier
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : '')) + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type + ' ' + this.match_type
+ ' ' + this.field_name + ' ' + this._field_name
+ ' ' + this.value_patterns + ';'; + ' ' + this.value_patterns + ';';
} }
@ -76,10 +85,10 @@ export class DeleteHeaderCommand extends ActionCommand
}); });
if (l && args[l-1] instanceof GrammarString) { if (l && args[l-1] instanceof GrammarString) {
this.field_name = args[l-1]; this._field_name = args[l-1];
this.value_patterns = args[l]; this.value_patterns = args[l];
} else { } else {
this.field_name = args[l]; this._field_name = args[l];
} }
} }
} }

View file

@ -8,11 +8,11 @@ import {
GrammarString GrammarString
} from 'Sieve/Grammar'; } from 'Sieve/Grammar';
class rfc5429Command extends ActionCommand class /*abstract*/ rfc5429Command extends ActionCommand
{ {
constructor(identifier) constructor()
{ {
super(identifier); super();
this._reason = new GrammarQuotedString; this._reason = new GrammarQuotedString;
} }
@ -44,7 +44,6 @@ class rfc5429Command extends ActionCommand
*/ */
export class ErejectCommand extends rfc5429Command export class ErejectCommand extends rfc5429Command
{ {
constructor() { super('ereject'); }
get require() { return 'ereject'; } get require() { return 'ereject'; }
} }
@ -53,6 +52,5 @@ export class ErejectCommand extends rfc5429Command
*/ */
export class RejectCommand extends rfc5429Command export class RejectCommand extends rfc5429Command
{ {
constructor() { super('reject'); }
get require() { return 'reject'; } get require() { return 'reject'; }
} }

View file

@ -26,14 +26,16 @@ export class NotifyCommand extends ActionCommand
} }
get method() { return this._method.value; } get method() { return this._method.value; }
get from() { return this._from.value; } set method(str) { this._method.value = str; }
get importance() { return this._importance.value; }
get message() { return this._message.value; }
set method(str) { this._method.value = str; } get from() { return this._from.value; }
set from(str) { this._from.value = str; } set from(str) { this._from.value = str; }
get importance() { return this._importance.value; }
set importance(int) { this._importance.value = int; } 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'; } get require() { return 'enotify'; }
@ -63,7 +65,8 @@ export class NotifyCommand extends ActionCommand
this.options = arg; // GrammarStringList this.options = arg; // GrammarStringList
} else if (i && ':' === args[i-1][0]) { } else if (i && ':' === args[i-1][0]) {
// :from, :importance, :message // :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() constructor()
{ {
super(); super();
this.notification_uri = new GrammarQuotedString; this._notification_uri = new GrammarQuotedString;
this.notification_capability = new GrammarQuotedString; this._notification_capability = new GrammarQuotedString;
this.key_list = new GrammarStringList; 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() toString()
{ {
return 'valid_notify_method ' return 'valid_notify_method '
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this.match_type ? ' ' + this.match_type : '') + (this._match_type ? ' ' + this._match_type : '')
+ this.notification_uri + (this.relational_match ? ' ' + this.relational_match : '')
+ this.notification_capability + this._notification_uri
+ this._notification_capability
+ this.key_list; + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
this.key_list = args.pop(); this.key_list = args.pop();
this.notification_capability = args.pop(); this._notification_capability = args.pop();
this.notification_uri = args.pop(); this._notification_uri = args.pop();
} }
} }

View file

@ -41,18 +41,21 @@ export class ErrorCommand extends ControlCommand
constructor() constructor()
{ {
super(); super();
this.message = new GrammarQuotedString; this._message = new GrammarQuotedString;
} }
get require() { return 'ihave'; } get require() { return 'ihave'; }
get message() { return this._message.value; }
set message(v) { this._message.value = v; }
toString() toString()
{ {
return 'error ' + this.message + ';'; return 'error ' + this._message + ';';
} }
pushArguments(args) pushArguments(args)
{ {
this.message = args.pop(); this._message = args.pop();
} }
} }

View file

@ -42,28 +42,35 @@ export class MetadataTest extends TestCommand
constructor() constructor()
{ {
super(); super();
this.mailbox = new GrammarQuotedString; this._mailbox = new GrammarQuotedString;
this.annotation_name = new GrammarQuotedString; this._annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
} }
get require() { return 'mboxmetadata'; } 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() toString()
{ {
return 'metadata ' return 'metadata '
+ ' ' + this.match_type + ' ' + this._match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.mailbox + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this.annotation_name + ' ' + this._mailbox
+ ' ' + this._annotation_name
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
this.key_list = args.pop(); this.key_list = args.pop();
this.annotation_name = args.pop(); this._annotation_name = args.pop();
this.mailbox = args.pop(); this._mailbox = args.pop();
} }
} }
@ -75,23 +82,26 @@ export class MetadataExistsTest extends TestCommand
constructor() constructor()
{ {
super(); super();
this.mailbox = new GrammarQuotedString; this._mailbox = new GrammarQuotedString;
this.annotation_names = new GrammarStringList; this.annotation_names = new GrammarStringList;
} }
get require() { return 'mboxmetadata'; } get require() { return 'mboxmetadata'; }
get mailbox() { return this._mailbox.value; }
set mailbox(v) { this._mailbox.value = v; }
toString() toString()
{ {
return 'metadataexists ' return 'metadataexists '
+ ' ' + this.mailbox + ' ' + this._mailbox
+ ' ' + this.annotation_names; + ' ' + this.annotation_names;
} }
pushArguments(args) pushArguments(args)
{ {
this.annotation_names = args.pop(); this.annotation_names = args.pop();
this.mailbox = args.pop(); this._mailbox = args.pop();
} }
} }
@ -103,25 +113,29 @@ export class ServerMetadataTest extends TestCommand
constructor() constructor()
{ {
super(); super();
this.annotation_name = new GrammarQuotedString; this._annotation_name = new GrammarQuotedString;
this.key_list = new GrammarStringList; this.key_list = new GrammarStringList;
} }
get require() { return 'servermetadata'; } get require() { return 'servermetadata'; }
get annotation_name() { return this._annotation_name.value; }
set annotation_name(v) { this._annotation_name.value = v; }
toString() toString()
{ {
return 'servermetadata ' return 'servermetadata '
+ ' ' + this.match_type + ' ' + this._match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + this.annotation_name + (this._comparator ? ' :comparator ' + this._comparator : '')
+ ' ' + this._annotation_name
+ ' ' + this.key_list; + ' ' + this.key_list;
} }
pushArguments(args) pushArguments(args)
{ {
this.key_list = args.pop(); this.key_list = args.pop();
this.annotation_name = args.pop(); this._annotation_name = args.pop();
} }
} }

View file

@ -5,6 +5,7 @@
import { import {
ActionCommand, ActionCommand,
ControlCommand, ControlCommand,
GrammarCommands,
GrammarNumber, GrammarNumber,
GrammarQuotedString, GrammarQuotedString,
GrammarString, GrammarString,
@ -19,7 +20,8 @@ export class ForEveryPartCommand extends ControlCommand
constructor() constructor()
{ {
super(); super();
this._name = new GrammarString; this.name = new GrammarString;
this.commands = new GrammarCommands;
} }
get require() { return 'foreverypart'; } get require() { return 'foreverypart'; }
@ -27,8 +29,8 @@ export class ForEveryPartCommand extends ControlCommand
toString() toString()
{ {
let result = 'foreverypart'; let result = 'foreverypart';
if (this._subject.length) { if (this.name.length) {
result += ' :name ' + this._name; result += ' :name ' + this.name;
} }
return result + ' ' + this.commands; return result + ' ' + this.commands;
} }
@ -37,19 +39,22 @@ export class ForEveryPartCommand extends ControlCommand
{ {
args.forEach((arg, i) => { args.forEach((arg, i) => {
if (':name' === arg) { 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 export class BreakCommand extends ForEveryPartCommand
{ {
toString() toString()
{ {
let result = 'break'; let result = 'break';
if (this._subject.length) { if (this.name.length) {
result += ' :name ' + this._name; result += ' :name ' + this.name;
} }
return result + ';'; return result + ';';
} }
@ -63,14 +68,23 @@ export class ReplaceCommand extends ActionCommand
constructor() constructor()
{ {
super(); super();
this.mime = false; this.mime = false;
this._subject = new GrammarQuotedString; this._subject = new GrammarQuotedString;
this._from = new GrammarQuotedString; this._from = new GrammarQuotedString;
this.replacement = new GrammarQuotedString; this._replacement = new GrammarQuotedString;
} }
get require() { return 'replace'; } 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() toString()
{ {
let result = 'replace'; let result = 'replace';
@ -82,20 +96,20 @@ export class ReplaceCommand extends ActionCommand
} }
if (this._from.length) { if (this._from.length) {
result += ' :from ' + this._from; result += ' :from ' + this._from;
// result += ' :from ' + this.arguments[':from'];
} }
return result + this.replacement + ';'; return result + this._replacement + ';';
} }
pushArguments(args) pushArguments(args)
{ {
this.replacement = args.pop(); this._replacement = args.pop();
args.forEach((arg, i) => { args.forEach((arg, i) => {
if (':mime' === arg) { if (':mime' === arg) {
this.mime = true; this.mime = true;
} else if (i && ':' === args[i-1][0]) { } else if (i && ':' === args[i-1][0]) {
// :subject, :from // :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 require() { return 'enclose'; }
get subject() { return this._subject.value; }
set subject(v) { this._subject.value = v; }
toString() toString()
{ {
let result = 'enclose'; let result = 'enclose';
@ -132,7 +149,8 @@ export class EncloseCommand extends ActionCommand
args.forEach((arg, i) => { args.forEach((arg, i) => {
if (i && ':' === args[i-1][0]) { if (i && ':' === args[i-1][0]) {
// :subject, :headers // :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 * 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 export class ExtractTextCommand extends ActionCommand
{ {
@ -148,9 +167,12 @@ export class ExtractTextCommand extends ActionCommand
super(); super();
this.modifiers = []; this.modifiers = [];
this._first = new GrammarNumber; 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'; } get require() { return 'extracttext'; }
toString() toString()
@ -160,12 +182,12 @@ export class ExtractTextCommand extends ActionCommand
if (0 < this._first.value) { if (0 < this._first.value) {
result += ' :first ' + this._first; result += ' :first ' + this._first;
} }
return result + ' ' + this.varname + ';'; return result + ' ' + this._varname + ';';
} }
pushArguments(args) pushArguments(args)
{ {
this.varname = args.pop(); this._varname = args.pop();
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => { [':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
args.includes(modifier) && this.modifiers.push(modifier); args.includes(modifier) && this.modifiers.push(modifier);
}); });

View file

@ -14,7 +14,7 @@ export class ValidExtListTest extends TestCommand
{ {
constructor() constructor()
{ {
super(); super('valid_ext_list');
this.ext_list_names = new GrammarStringList; this.ext_list_names = new GrammarStringList;
} }

View file

@ -16,18 +16,21 @@ export class IncludeCommand extends ControlCommand
this.global = false; // ':personal' / ':global'; this.global = false; // ':personal' / ':global';
this.once = false; this.once = false;
this.optional = false; this.optional = false;
this.value = new GrammarQuotedString; this._value = new GrammarQuotedString;
} }
get require() { return 'include'; } get require() { return 'include'; }
get value() { return this._value.value; }
set value(v) { this._value.value = v; }
toString() toString()
{ {
return 'include' return 'include'
+ (this.global ? ' :global' : '') + (this.global ? ' :global' : '')
+ (this.once ? ' :once' : '') + (this.once ? ' :once' : '')
+ (this.optional ? ' :optional' : '') + (this.optional ? ' :optional' : '')
+ ' ' + this.value + ';'; + ' ' + this._value + ';';
} }
pushArguments(args) pushArguments(args)
@ -36,7 +39,7 @@ export class IncludeCommand extends ControlCommand
if (':global' === arg || ':once' === arg || ':optional' === arg) { if (':global' === arg || ':once' === arg || ':optional' === arg) {
this[arg.slice(1)] = true; this[arg.slice(1)] = true;
} else if (arg instanceof GrammarQuotedString) { } else if (arg instanceof GrammarQuotedString) {
this.value = arg; this._value = arg;
} }
}); });
} }

View file

@ -9,7 +9,7 @@ import {
MULTILINE_DOTSTART MULTILINE_DOTSTART
} from 'Sieve/RegEx'; } from 'Sieve/RegEx';
import { arrayToString, getMatchTypes } from 'Sieve/Utils'; import { arrayToString, getMatchTypes, getComparators } from 'Sieve/Utils';
/** /**
* abstract * abstract
@ -18,7 +18,7 @@ export class GrammarString /*extends String*/
{ {
constructor(value = '') constructor(value = '')
{ {
this._value = value; this._value = value.toString ? value.toString() : value;
} }
toString() toString()
@ -45,41 +45,41 @@ export class GrammarString /*extends String*/
/** /**
* abstract * 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 * https://tools.ietf.org/html/rfc5228#section-2.9
*/ */
export class GrammarCommand const cmdNameSuffix = /(test|command|action)$/;
export /*abstract*/ class GrammarCommand
{ {
constructor(identifier) constructor(identifier)
{ {
this.identifier = identifier || this.constructor.name.toLowerCase().replace(/(test|command|action)$/, ''); /*
this.arguments = []; if (this.constructor == GrammarCommand) {
this.commands = new GrammarCommands; throw Error("Abstract class can't be instantiated.");
}
*/
this.identifier = identifier || this.constructor.name.toLowerCase().replace(cmdNameSuffix, '');
} }
toString() toString()
{ {
let result = this.identifier; let result = this.identifier;
if (this.arguments.length) { if (this.arguments?.length) {
result += ' ' + arrayToString(this.arguments, ' '); result += ' ' + arrayToString(this.arguments, ' ');
} }
return result + ( return result + ';';
this.commands.length ? ' ' + this.commands : ';'
);
}
getComparators()
{
return ['i;ascii-casemap'];
}
getMatchTypes()
{
return [':is', ':contains', ':matches'];
} }
pushArguments(args) pushArguments(args)
@ -108,78 +108,107 @@ export class GrammarCommands extends Array
/** /**
* https://tools.ietf.org/html/rfc5228#section-3 * https://tools.ietf.org/html/rfc5228#section-3
*/ */
export class ControlCommand extends GrammarCommand export /*abstract*/ class ControlCommand extends GrammarCommand
{ {
/*
constructor(identifier) constructor(identifier)
{ {
super(identifier); if (this.constructor == ControlCommand) {
this.commands = new GrammarCommands; throw Error("Abstract class can't be instantiated.");
}
toString()
{
let result = this.identifier;
if (this.arguments.length) {
result += ' ' + arrayToString(this.arguments, ' ');
} }
return result + ( super(identifier);
this.commands.length ? ' ' + this.commands : ';'
);
}
getComparators()
{
return ['i;ascii-casemap'];
}
getMatchTypes()
{
return [':is', ':contains', ':matches'];
} }
*/
} }
/** /**
* https://tools.ietf.org/html/rfc5228#section-4 * https://tools.ietf.org/html/rfc5228#section-4
*/ */
export class ActionCommand extends GrammarCommand export /*abstract*/ class ActionCommand extends GrammarCommand
{ {
/*
constructor(identifier) constructor(identifier)
{ {
if (this.constructor == ActionCommand) {
throw Error("Abstract class can't be instantiated.");
}
super(identifier); 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 * https://tools.ietf.org/html/rfc5228#section-5
*/ */
export class TestCommand extends GrammarCommand export /*abstract*/ class TestCommand extends GrammarCommand
{ {
constructor(identifier) constructor(identifier)
{ {
/*
if (this.constructor == TestCommand) {
throw Error("Abstract class can't be instantiated.");
}
*/
super(identifier); super(identifier);
// Almost every test has a comparator and match_type, so define them here // Almost every test has a comparator and match_type, so define them here
this.comparator = ''; this._comparator = '';
this.match_type = ':is'; 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() 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 return (this.identifier
+ (this.comparator ? ' :comparator ' + this.comparator : '') + (this._comparator ? ' :comparator ' + this._comparator : '')
+ (this.match_type ? ' ' + this.match_type : '') + (this._match_type ? ' ' + this._match_type : '')
+ (this.relational_match ? ' ' + this.relational_match : '')
+ ' ' + arrayToString(this.arguments, ' ')).trim(); + ' ' + arrayToString(this.arguments, ' ')).trim();
} }
} }
@ -251,6 +280,7 @@ export class GrammarStringList extends Array
{ {
toString() toString()
{ {
// if there is only a single string, the brackets are optional
if (1 < this.length) { if (1 < this.length) {
return '[' + this.join(',') + ']'; return '[' + this.join(',') + ']';
} }
@ -259,7 +289,7 @@ export class GrammarStringList extends Array
push(value) push(value)
{ {
if (!(value instanceof GrammarString)) { if (!(value instanceof GrammarQuotedString)) {
value = new GrammarQuotedString(value); value = new GrammarQuotedString(value);
} }
super.push(value); super.push(value);
@ -286,10 +316,14 @@ GrammarStringList.fromString = list => {
export class GrammarQuotedString extends GrammarString export class GrammarQuotedString extends GrammarString
{ {
constructor(value = '')
{
super(value instanceof GrammarQuotedString ? value.value : value);
}
toString() toString()
{ {
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"'; return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
} }
} }
@ -324,3 +358,23 @@ GrammarMultiLine.fromString = string => {
} }
return new GrammarMultiLine(); 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 : ';'
);
}
}

View file

@ -18,6 +18,7 @@ import {
import { import {
GrammarBracketComment, GrammarBracketComment,
GrammarCommand, GrammarCommand,
GrammarComment,
GrammarHashComment, GrammarHashComment,
GrammarMultiLine, GrammarMultiLine,
GrammarNumber, GrammarNumber,
@ -27,8 +28,8 @@ import {
GrammarTestList GrammarTestList
} from 'Sieve/Grammar'; } from 'Sieve/Grammar';
import { availableCommands } from 'Sieve/Commands'; import { availableCommands, unavailableCommands } from 'Sieve/Commands';
import { ConditionalCommand, RequireCommand } from 'Sieve/Commands/Controls'; import { ConditionalCommand, IfCommand, RequireCommand } from 'Sieve/Commands/Controls';
import { NotTest } from 'Sieve/Commands/Tests'; import { NotTest } from 'Sieve/Commands/Tests';
const const
@ -92,6 +93,7 @@ export const parseScript = (script, name = 'script.sieve') => {
// Only activate available commands // Only activate available commands
const Commands = availableCommands(); const Commands = availableCommands();
const disabledCommands = unavailableCommands();
let match, let match,
line = 1, line = 1,
@ -117,18 +119,20 @@ export const parseScript = (script, name = 'script.sieve') => {
let prev_arg = args[args.length-1]; let prev_arg = args[args.length-1];
if (getMatchTypes(0).includes(arg)) { if (getMatchTypes(0).includes(arg)) {
command.match_type = arg; command.match_type = arg;
} else if (':value' === prev_arg || ':count' === prev_arg) { } else if (getMatchTypes(0).includes(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;
--args.length; --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) { } else if (':comparator' === prev_arg) {
command.comparator = arg; command.comparator = arg;
--args.length; --args.length;
} else {
args.push(arg);
} }
args.push(arg);
}, },
pushArgs = () => { pushArgs = () => {
if (args.length) { if (args.length) {
@ -154,27 +158,30 @@ export const parseScript = (script, name = 'script.sieve') => {
pushArgs(); pushArgs();
value = value.toLowerCase(); value = value.toLowerCase();
let new_command; let new_command;
if ('if' === value) { if (Commands[value]) {
new_command = new ConditionalCommand(value); if ('elsif' === value || 'else' === value) {
} else if ('elsif' === value || 'else' === value) { let valid = false, cmd = (command ? command?.commands : tree), i = cmd?.length;
// (prev_command instanceof ConditionalCommand) || error('Not after IF condition'); while (i) {
new_command = new ConditionalCommand(value); cmd[--i];
} else if (Commands[value]) { 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) { if ('allof' === value || 'anyof' === value) {
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional'); // (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
} }
new_command = new Commands[value](); new_command = new Commands[value]();
} else if (disabledCommands[value]) {
console.error('Unsupported command: ' + value);
new_command = new disabledCommands[value]();
} else { } else {
if (command && ( console.error('Unknown command: ' + value);
command instanceof ConditionalCommand new_command = new GrammarCommand(value);
|| 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);
}
} }
if (new_command instanceof TestCommand) { if (new_command instanceof TestCommand) {
@ -192,7 +199,7 @@ export const parseScript = (script, name = 'script.sieve') => {
if (command.commands) { if (command.commands) {
command.commands.push(new_command); command.commands.push(new_command);
} else { } else {
error('commands not allowed in "' + command.identifier + '" command'); error('command "' + new_command.identifier + '" not allowed in "' + command.identifier + '" command');
} }
} else { } else {
tree.push(new_command); tree.push(new_command);
@ -219,7 +226,8 @@ export const parseScript = (script, name = 'script.sieve') => {
pushArg(GrammarMultiLine.fromString(value)); pushArg(GrammarMultiLine.fromString(value));
break; break;
case T_QUOTED_STRING: 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; break;
case T_NUMBER: case T_NUMBER:
pushArg(new GrammarNumber(value)); pushArg(new GrammarNumber(value));
@ -268,7 +276,6 @@ export const parseScript = (script, name = 'script.sieve') => {
break; break;
case T_BLOCK_END: case T_BLOCK_END:
(command instanceof ConditionalCommand) || error(TokenError[type]); (command instanceof ConditionalCommand) || error(TokenError[type]);
// prev_command = command;
command = levels.up(); command = levels.up();
break; break;
@ -293,5 +300,6 @@ export const parseScript = (script, name = 'script.sieve') => {
} }
tree.requires = requires; tree.requires = requires;
tree.toString = () => tree.join('\r\n');
return tree; return tree;
}; };

View file

@ -22,7 +22,7 @@ export const
koComputable = fn => ko.computed(fn, {'pure':true}), koComputable = fn => ko.computed(fn, {'pure':true}),
arrayToString = (arr, separator) => arrayToString = (arr, separator) =>
arr.map(item => item.toString?.() || item).join(separator), (arr || []).map(item => item.toString?.() || item).join(separator),
/* /*
getNotificationMessage = code => { getNotificationMessage = code => {
let key = getKeyByValue(Notifications, code); let key = getKeyByValue(Notifications, code);
@ -58,11 +58,33 @@ export const
serverErrorDesc(text); 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) => { getMatchTypes = (validOnly = 1) => {
let result = [':is',':contains',':matches']; let result = [':is',':contains',':matches'];
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3 // https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
// Only available for tests with a key_list property
if (capa.includes('extlists') || !validOnly) { if (capa.includes('extlists') || !validOnly) {
result.push(':list'); result.push(':list');
} }
if (capa.includes('relational') || !validOnly) {
result.push(':value');
result.push(':count');
}
return result; return result;
}; };

View file

@ -30,8 +30,6 @@ const
sDeepPrefix = '\u00A0\u00A0\u00A0', sDeepPrefix = '\u00A0\u00A0\u00A0',
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/, showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
foldersWalk = folders => { foldersWalk = folders => {
folders.forEach(oItem => { folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) { if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
@ -39,7 +37,7 @@ const
id: oItem.fullName, id: oItem.fullName,
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(), name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
system: false, 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.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.folderSelectList = koComputable(() => folderListOptionsBuilder()); this.folderSelectList = koComputable(folderListOptionsBuilder);
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false)); this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));

View file

@ -78,7 +78,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
if (iError) { if (iError) {
self.saveError(true); self.saveError(true);
self.errorText(data?.ErrorMessageAdditional || getNotification(iError)); self.errorText(data?.messageAdditional || getNotification(iError));
} else { } else {
script.exists() || scripts.push(script); script.exists() || scripts.push(script);
script.exists(true); script.exists(true);

View file

@ -23,7 +23,7 @@ try {
data = data ? decodeURIComponent(data[2]) : null; data = data ? decodeURIComponent(data[2]) : null;
data = data ? JSON.parse(data) : {}; data = data ? JSON.parse(data) : {};
win[sName] = { win[sName] = {
getItem: key => data[key] == null ? null : data[key], getItem: key => data[key] ?? null,
setItem: (key, value) => { setItem: (key, value) => {
data[key] = ''+value; // forces the value to a string data[key] = ''+value; // forces the value to a string
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data)) document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))

View file

@ -1,8 +1,155 @@
import { AskPopupView } from 'View/Popup/Ask'; 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(); export const Passphrases = new WeakMap();
Passphrases.ask = async (key, sAskDesc, btnText) => Passphrases.ask = async (key, sAskDesc, btnText) => {
Passphrases.has(key) if (Passphrases.has(key)) {
? {password:Passphrases.get(key)/*, remember:false*/} return { password: Passphrases.handle(key)/*, remember:false*/ };
: await AskPopupView.password(sAskDesc, btnText); } 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']
);
});
};

View file

@ -2,8 +2,6 @@ import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
export const AccountUserStore = koArrayWithDestroy(); export const AccountUserStore = koArrayWithDestroy();
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
addObservablesTo(AccountUserStore, { addObservablesTo(AccountUserStore, {
email: '', email: '',
loading: false loading: false

View file

@ -39,7 +39,7 @@ ContactUserStore.sync = fResultFunc => {
line = JSON.parse(line); line = JSON.parse(line);
if ('ContactsSync' === line.Action) { if ('ContactsSync' === line.Action) {
ContactUserStore.syncing(false); ContactUserStore.syncing(false);
fResultFunc?.(line.ErrorCode, line); fResultFunc?.(line.code, line);
} }
} catch (e) { } catch (e) {
ContactUserStore.syncing(false); ContactUserStore.syncing(false);

View file

@ -12,6 +12,8 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases'; import { Passphrases } from 'Storage/Passphrases';
import { baseCollator } from 'Common/Translator';
const const
findGnuPGKey = (keys, query/*, sign*/) => findGnuPGKey = (keys, query/*, sign*/) =>
keys.find(key => keys.find(key =>
@ -49,13 +51,20 @@ export const GnuPGUserStore = new class {
key.for = email => aEmails.includes(IDN.toASCII(email)); key.for = email => aEmails.includes(IDN.toASCII(email));
key.askDelete = ko.observable(false); key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper(); 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 = () => { key.remove = () => {
if (key.askDelete()) { if (key.askDelete()) {
Remote.request('GnupgDeleteKey', Remote.request('GnupgDeleteKey',
(iError, oData) => { (iError, oData) => {
if (oData) { if (oData) {
if (iError) { if (iError) {
alert(oData.ErrorMessage); alert(oData.message);
} else if (oData.Result) { } else if (oData.Result) {
isPrivate isPrivate
? this.privateKeys.remove(key) ? this.privateKeys.remove(key)
@ -67,6 +76,7 @@ export const GnuPGUserStore = new class {
isPrivate: isPrivate isPrivate: isPrivate
} }
); );
isPrivate && key.forgetPass();
} }
}; };
if (isPrivate) { if (isPrivate) {
@ -75,7 +85,7 @@ export const GnuPGUserStore = new class {
'GnuPG key<br>' + key.id + ' ' + key.emails[0], 'GnuPG key<br>' + key.id + ' ' + key.emails[0],
btnTxt btnTxt
); );
pass && pass.remember && Passphrases.set(key, pass.password); pass && pass.remember && Passphrases.handle(key, pass.password);
return pass?.password; return pass?.password;
}; };
} }
@ -106,7 +116,7 @@ export const GnuPGUserStore = new class {
key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key])); key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key]));
return key; return key;
}, },
collator = new Intl.Collator(undefined, {sensitivity: 'base'}), collator = baseCollator(),
sort = keys => keys.sort( sort = keys => keys.sort(
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id) (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; return fingerprints;
} }
getPrivateKeyFor(query, sign) { getPrivateKeyFor(query/*, sign*/) {
return findGnuPGKey(this.privateKeys, query, sign); return findGnuPGKey(this.privateKeys, query/*, sign*/);
} }
async decrypt(message) { async decrypt(message) {

View file

@ -1,5 +1,12 @@
import { koArrayWithDestroy } from 'External/ko'; import { koArrayWithDestroy, koComputable } from 'External/ko';
import { isArray } from 'Common/Utils';
export const IdentityUserStore = koArrayWithDestroy(); export const IdentityUserStore = koArrayWithDestroy();
IdentityUserStore.loading = ko.observable(false).extend({ debounce: 100 }); 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;
});

View file

@ -32,8 +32,11 @@ import { SettingsGet } from 'Common/Globals';
import { SUB_QUERY_PREFIX } from 'Common/Links'; import { SUB_QUERY_PREFIX } from 'Common/Links';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { baseCollator } from 'Common/Translator';
const const
isChecked = item => item.checked(), isChecked = item => item.checked(),
isDeleted = item => item.isDeleted(),
replaceHash = hash => { replaceHash = hash => {
rl.route.off(); rl.route.off();
hasher.replaceHash(hash); hasher.replaceHash(hash);
@ -105,9 +108,8 @@ addComputablesTo(MessagelistUserStore, {
listCheckedOrSelected: () => { listCheckedOrSelected: () => {
const const
selectedMessage = MessagelistUserStore.selectedMessage(), selectedMessage = MessagelistUserStore.selectedMessage(),
focusedMessage = MessagelistUserStore.focusedMessage(),
checked = MessagelistUserStore.filter(item => isChecked(item)); checked = MessagelistUserStore.filter(item => isChecked(item));
return checked.length ? checked : (selectedMessage || focusedMessage ? [selectedMessage || focusedMessage] : []); return checked.length ? checked : (selectedMessage ? [selectedMessage] : []);
}, },
listCheckedOrSelectedUidsWithSubMails: () => { listCheckedOrSelectedUidsWithSubMails: () => {
@ -135,11 +137,18 @@ MessagelistUserStore.hasChecked = koComputable(
MessagelistUserStore.hasCheckedOrSelected = koComputable(() => MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
!!MessagelistUserStore.selectedMessage() !!MessagelistUserStore.selectedMessage()
| !!MessagelistUserStore.focusedMessage()
// Issue: not all are observed? // Issue: not all are observed?
| !!MessagelistUserStore.find(isChecked) | !!MessagelistUserStore.find(isChecked)
).extend({ rateLimit: 50 }); ).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) => { MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
if (getFolderInboxName() === folder && arrayLength(newMessages)) { if (getFolderInboxName() === folder && arrayLength(newMessages)) {
@ -254,17 +263,14 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
let flags = folderInfo.permanentFlags || []; let flags = folderInfo.permanentFlags || [];
if (flags.includes('\\*')) { if (flags.includes('\\*')) {
/** Add Thunderbird labels */
let i = 6; let i = 6;
while (--i) { while (--i) {
flags.includes('$label'+i) || flags.push('$label'+i); flags.includes('$label'+i) || flags.push('$label'+i);
} }
/** TODO: add others by default? */
} }
flags.sort((a, b) => { folder.permanentFlags(flags.sort(baseCollator().compare));
a = a.toUpperCase();
b = b.toUpperCase();
return (a < b) ? -1 : ((a > b) ? 1 : 0);
});
folder.permanentFlags(flags);
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages); MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
} }
@ -314,6 +320,7 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) { if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.useThreads = 1; params.useThreads = 1;
params.threadAlgorithm = SettingsUserStore.threadAlgorithm();
params.threadUid = MessagelistUserStore.threadUid(); params.threadUid = MessagelistUserStore.threadUid();
} else { } else {
params.threadUid = 0; params.threadUid = 0;
@ -370,6 +377,14 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
messages.forEach(oMessage => messages.forEach(oMessage =>
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged') 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(); rootUids = rootUids.validUnique();
length = rootUids.length; length = rootUids.length;
@ -399,6 +414,15 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
setAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0 setAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
}); });
break; 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 // no default
} }
} }
@ -444,23 +468,6 @@ MessagelistUserStore.moveMessages = (
if (page > MessagelistUserStore.pageCount()) { if (page > MessagelistUserStore.pageCount()) {
setPage = 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) { if (setPage) {
MessagelistUserStore.page(setPage); MessagelistUserStore.page(setPage);
replaceHash( replaceHash(
@ -518,7 +525,6 @@ MessagelistUserStore.moveMessages = (
currentMessage = null; currentMessage = null;
MessageUserStore.message(null); MessageUserStore.message(null);
} }
item.deleted(true);
MessagelistUserStore.remove(item); MessagelistUserStore.remove(item);
}); });
} }

View file

@ -13,7 +13,11 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases'; import { Passphrases } from 'Storage/Passphrases';
import { baseCollator, getNotification } from 'Common/Translator';
const const
loaded = () => !!window.openpgp,
findOpenPGPKey = (keys, query/*, sign*/) => findOpenPGPKey = (keys, query/*, sign*/) =>
keys.find(key => keys.find(key =>
key.for(query) || query == key.id || query == key.fingerprint key.for(query) || query == key.id || query == key.fingerprint
@ -34,12 +38,12 @@ const
privateKey: privateKey.key, privateKey: privateKey.key,
passphrase passphrase
}); });
result && pass.remember && Passphrases.set(privateKey, passphrase); result && pass.remember && Passphrases.handle(privateKey, passphrase);
return result; return result;
} }
}, },
collator = new Intl.Collator(undefined, {sensitivity: 'base'}), collator = baseCollator(),
sort = keys => keys.sort( 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.fingerprint, b.fingerprint)
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id) (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', publicKeysItem = 'openpgp-public-keys',
privateKeysItem = 'openpgp-private-keys', privateKeysItem = 'openpgp-private-keys',
storage = window.localStorage,
loadOpenPgpKeys = async itemname => { loadOpenPgpKeys = async itemname => {
let keys = [], key, let keys = [], key,
armoredKeys = JSON.parse(storage.getItem(itemname)), armoredKeys = JSON.parse(localStorage.getItem(itemname)),
i = arrayLength(armoredKeys); i = arrayLength(armoredKeys);
while (i--) try { while (i--) try {
key = await openpgp.readKey({armoredKey:armoredKeys[i]}); key = await openpgp.readKey({armoredKey:armoredKeys[i]});
@ -70,9 +73,9 @@ const
storeOpenPgpKeys = (keys, section) => { storeOpenPgpKeys = (keys, section) => {
let armoredKeys = keys.map(item => item.armor); let armoredKeys = keys.map(item => item.armor);
if (armoredKeys.length) { if (armoredKeys.length) {
storage.setItem(section, JSON.stringify(armoredKeys)); localStorage.setItem(section, JSON.stringify(armoredKeys));
} else { } else {
storage.removeItem(section); localStorage.removeItem(section);
} }
}; };
@ -87,6 +90,9 @@ class OpenPgpKeyModel {
this.armor = armor; this.armor = armor;
this.askDelete = ko.observable(false); this.askDelete = ko.observable(false);
this.openForDeletion = ko.observable(null).askDeleteHelper(); 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.getUserIDs()
// key.getPrimaryUser() // key.getPrimaryUser()
} }
@ -112,12 +118,29 @@ class OpenPgpKeyModel {
if (this.key.isPrivate()) { if (this.key.isPrivate()) {
OpenPGPUserStore.privateKeys.remove(this); OpenPGPUserStore.privateKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem); storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
this.forgetPass()
} else { } else {
OpenPGPUserStore.publicKeys.remove(this); OpenPGPUserStore.publicKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem); 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() { toJSON() {
return this.armor; return this.armor;
@ -132,23 +155,36 @@ export const OpenPGPUserStore = new class {
} }
loadKeyrings() { loadKeyrings() {
if (window.openpgp) { if (loaded()) {
loadOpenPgpKeys(publicKeysItem).then(keys => { loadOpenPgpKeys(publicKeysItem)
.then(keys => {
this.publicKeys(dedup(keys)); this.publicKeys(dedup(keys));
console.log('openpgp.js public keys loaded'); console.log('openpgp.js public keys loaded');
}); })
loadOpenPgpKeys(privateKeysItem).then(keys => { .finally(() => {
this.privateKeys(dedup(keys)); loadOpenPgpKeys(privateKeysItem)
console.log('openpgp.js private keys loaded'); .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} * @returns {boolean}
*/ */
isSupported() { isSupported() {
return !!window.openpgp; return loaded();
} }
importKey(armoredKey) { importKey(armoredKey) {
@ -156,7 +192,7 @@ export const OpenPGPUserStore = new class {
} }
async importKeys(keys) { async importKeys(keys) {
if (window.openpgp) { if (loaded()) {
const privateKeys = this.privateKeys(), const privateKeys = this.privateKeys(),
publicKeys = this.publicKeys(); publicKeys = this.publicKeys();
for (const armoredKey of keys) try { for (const armoredKey of keys) try {
@ -183,7 +219,7 @@ export const OpenPGPUserStore = new class {
keyPair.revocationCertificate keyPair.revocationCertificate
*/ */
storeKeyPair(keyPair) { storeKeyPair(keyPair) {
if (window.openpgp) { if (loaded()) {
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => { openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key)); this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
storeOpenPgpKeys(this.publicKeys, publicKeysItem); storeOpenPgpKeys(this.publicKeys, publicKeysItem);

View file

@ -61,7 +61,7 @@ export const
if (gnuPG && oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) { if (gnuPG && oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
GnuPGUserStore.loadKeyrings(); GnuPGUserStore.loadKeyrings();
} }
iError && alert(oData.ErrorMessage); iError && alert(oData.message);
}, { }, {
key, gnuPG, backup key, gnuPG, backup
} }

View file

@ -1,4 +1,5 @@
import { addObservablesTo, koArrayWithDestroy } from 'External/ko'; import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
import { baseCollator } from 'Common/Translator';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
export const SMimeUserStore = koArrayWithDestroy(); export const SMimeUserStore = koArrayWithDestroy();
@ -12,7 +13,7 @@ SMimeUserStore.loadCertificates = () => {
SMimeUserStore.loading(true); SMimeUserStore.loading(true);
Remote.request('SMimeGetCertificates', (iError, oData) => { Remote.request('SMimeGetCertificates', (iError, oData) => {
SMimeUserStore.loading(false); SMimeUserStore.loading(false);
const collator = new Intl.Collator(undefined, {sensitivity: 'base'}); const collator = baseCollator();
iError || SMimeUserStore(oData.Result.sort( iError || SMimeUserStore(oData.Result.sort(
(a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t) (a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t)
)); ));

View file

@ -5,6 +5,7 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { pInt } from 'Common/Utils'; import { pInt } from 'Common/Utils';
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals'; import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { FolderUserStore } from 'Stores/User/Folder';
export const SettingsUserStore = new class { export const SettingsUserStore = new class {
constructor() { constructor() {
@ -22,6 +23,7 @@ export const SettingsUserStore = new class {
allowStyles: 0, allowStyles: 0,
collapseBlockquotes: 1, collapseBlockquotes: 1,
maxBlockquotesLevel: 0, maxBlockquotesLevel: 0,
minFontSize: 0,
listInlineAttachments: 0, listInlineAttachments: 0,
simpleAttachmentsList: 0, simpleAttachmentsList: 0,
useCheckboxesInList: 1, useCheckboxesInList: 1,
@ -29,11 +31,14 @@ export const SettingsUserStore = new class {
showNextMessage: 0, showNextMessage: 0,
allowDraftAutosave: 1, allowDraftAutosave: 1,
useThreads: 0, useThreads: 0,
defaultSort: '',
threadAlgorithm: '',
replySameFolder: 0, replySameFolder: 0,
hideUnsubscribed: 0, hideUnsubscribed: 0,
hideDeleted: 1, hideDeleted: 1,
unhideKolabFolders: 0, unhideKolabFolders: 0,
autoLogout: 0, autoLogout: 0,
keyPassForget: 15,
showUnreadCount: 0, showUnreadCount: 0,
messageNewWindow: 0, messageNewWindow: 0,
messageReadAuto: 0, messageReadAuto: 0,
@ -48,6 +53,7 @@ export const SettingsUserStore = new class {
layout: 1, layout: 1,
editorDefaultType: 'Html', editorDefaultType: 'Html',
editorWysiwyg: 'Squire', editorWysiwyg: 'Squire',
markdown: 0,
msgDefaultAction: 1 msgDefaultAction: 1
}); });
@ -93,13 +99,16 @@ export const SettingsUserStore = new class {
'AllowStyles', 'AllowStyles',
'CollapseBlockquotes', 'CollapseBlockquotes',
'MaxBlockquotesLevel', 'MaxBlockquotesLevel',
'minFontSize',
'ListInlineAttachments', 'ListInlineAttachments',
'simpleAttachmentsList', 'simpleAttachmentsList',
'UseCheckboxesInList', 'UseCheckboxesInList',
'listGrouped', 'listGrouped',
'showNextMessage', 'showNextMessage',
'AllowDraftAutosave', 'AllowDraftAutosave',
'UseThreads', 'useThreads',
'defaultSort',
'threadAlgorithm',
'ReplySameFolder', 'ReplySameFolder',
'HideUnsubscribed', 'HideUnsubscribed',
'HideDeleted', 'HideDeleted',
@ -110,7 +119,41 @@ export const SettingsUserStore = new class {
'requireTLS', 'requireTLS',
'pgpSign', 'pgpSign',
'pgpEncrypt', '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 => { ].forEach(name => {
let value = SettingsGet(name); let value = SettingsGet(name);
name = name[0].toLowerCase() + name.slice(1); name = name[0].toLowerCase() + name.slice(1);
@ -122,5 +165,8 @@ export const SettingsUserStore = new class {
self.checkMailInterval(pInt(SettingsGet('CheckMailInterval'))); self.checkMailInterval(pInt(SettingsGet('CheckMailInterval')));
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay'))); self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
self.autoLogout(pInt(SettingsGet('AutoLogout'))); self.autoLogout(pInt(SettingsGet('AutoLogout')));
self.keyPassForget(pInt(SettingsGet('keyPassForget')));
FolderUserStore.sortMode(self.defaultSort());
} }
}; };

View file

@ -34,4 +34,9 @@
display: block; display: block;
} }
.tab-content {
grid-column-end: 3;
border: 1px solid var(--border-color, #ddd);
padding: 10px;
}
} }

View file

@ -1,6 +1,6 @@
#V-PopupsLanguages { #V-PopupsLanguages {
max-width: 710px; max-width: 720px;
label { label {
display: inline-block; display: inline-block;

View file

@ -9,7 +9,8 @@
#V-PopupsCompose { #V-PopupsCompose {
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
max-width: 1000px; max-width: min(99vw, 1600px);
resize: horizontal;
width: 99%; width: 99%;
.modal-body { .modal-body {
@ -21,12 +22,18 @@
.tabs { .tabs {
height: 100%; height: 100%;
max-height: 100%;
overflow: auto;
} }
.tabs > label > * + * { .tabs > label > * + * {
margin-left: 0.5em; margin-left: 0.5em;
} }
.tabs .tab-content {
overflow: auto;
}
.textAreaParent, .attachmentAreaParent { .textAreaParent, .attachmentAreaParent {
height: 100%; height: 100%;
min-height: 200px; min-height: 200px;

View file

@ -125,8 +125,9 @@
/* Right */ /* Right */
.right { .right {
width: 100%;
border-left: 1px solid var(--border-color, #ddd); border-left: 1px solid var(--border-color, #ddd);
max-width: 600px;
width: 100%;
} }
.b-view-content-toolbar { .b-view-content-toolbar {

View file

@ -82,7 +82,7 @@
border-left: 3px solid transparent; border-left: 3px solid transparent;
color: var(--folders-color, #333); color: var(--folders-color, #333);
display: block; display: block;
line-height: 34px; line-height: 2.2em;
padding: 0 2em 0 @folderItemPadding; padding: 0 2em 0 @folderItemPadding;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
@ -151,7 +151,7 @@
color: var(--unread-count-color, #fff); color: var(--unread-count-color, #fff);
font-size: 80%; font-size: 80%;
line-height: 1.5em; line-height: 1.5em;
margin-top: 7px; margin-top: 0.5em;
min-width: 1.7em; min-width: 1.7em;
padding: 1px 4px; padding: 1px 4px;
text-align: center; text-align: center;

View file

@ -207,10 +207,6 @@
font-weight: bolder; font-weight: bolder;
} }
&.deleted {
opacity: .3;
}
.messageCheckbox { .messageCheckbox {
font-size: 114%; font-size: 114%;
padding: 5px .5em 0; padding: 5px .5em 0;
@ -327,6 +323,7 @@
.attachmentParent, .attachmentParent,
.flagParent { .flagParent {
margin: 0 10px 0 5px; margin: 0 10px 0 5px;
min-width: 1em;
} }
.flagParent::after { .flagParent::after {
content: '☆'; /*⚐*/ content: '☆'; /*⚐*/

View file

@ -45,4 +45,8 @@
content: ')'; content: ')';
} }
.identities-list tr + tr .identity-default {
display: none;
}
} }

View file

@ -56,6 +56,12 @@
.squire-wysiwyg { .squire-wysiwyg {
font-family: var(--fontSans); font-family: var(--fontSans);
/* Prevent collapse of empty elements so that the caret can be placed in there
*:empty::before {
content: "\200B";
}
*/
ul { ul {
padding-left: 40px; padding-left: 40px;
li { li {

View file

@ -16,7 +16,7 @@
text-align: var(--left, left); text-align: var(--left, left);
top: 100%; top: 100%;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
white-space: nowrap; white-space: pre;
z-index: 2001; z-index: 2001;
} }
@media screen and (max-width: @maxMobileWidth) { @media screen and (max-width: @maxMobileWidth) {
@ -33,6 +33,11 @@ input:-webkit-autofill:active {
transition: color 9999s ease-out, background-color 9999s ease-out; transition: color 9999s ease-out, background-color 9999s ease-out;
} }
.btn-rotate,
[data-icon].rotate::before {
transform: rotate(-180deg);
}
/* /*
@media (pointer: coarse), (hover: none) { @media (pointer: coarse), (hover: none) {
[title] { [title] {

View file

@ -36,7 +36,7 @@ export class AccountPopupView extends AbstractViewPopup {
this.submitRequest(false); this.submitRequest(false);
if (iError) { if (iError) {
this.submitError(getNotification(iError)); this.submitError(getNotification(iError));
this.submitErrorAdditional(data?.ErrorMessageAdditional); this.submitErrorAdditional(data?.messageAdditional);
} else { } else {
loadAccountsAndIdentities(); loadAccountsAndIdentities();
this.close(); this.close();

View file

@ -18,7 +18,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
text: '', text: '',
keyword: '', keyword: '',
repliedValue: -1, repliedValue: -1,
selectedDateValue: -1, selectedDateValue: 0,
selectedTreeValue: '', selectedTreeValue: '',
hasAttachment: false, hasAttachment: false,
@ -32,14 +32,12 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
// Almost the same as MessageModel.tagOptions // Almost the same as MessageModel.tagOptions
keywords: () => { keywords: () => {
const keywords = [{value:'',label:''}]; const keywords = [{value:'',label:''}];
FolderUserStore.currentFolder().permanentFlags.forEach(value => { FolderUserStore.currentFolder().optionalTags().forEach(value => {
if (isAllowedKeyword(value)) { let lower = value.toLowerCase();
let lower = value.toLowerCase(); keywords.push({
keywords.push({ value: value,
value: value, label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
label: i18n('MESSAGE_TAGS/'+lower, 0, lower) });
});
}
}); });
return keywords return keywords
}, },
@ -57,15 +55,25 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
selectedDates: () => { selectedDates: () => {
translateTrigger(); 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 [ return [
{ id: -1, name: i18n(prefix + 'ALL') }, { id: 0, name: i18n('SEARCH/DATE_ALL') },
{ id: 3, name: i18n(prefix + '3_DAYS') }, { id: 3, name: i18n(prefix + '3_DAYS') },
{ id: 7, name: i18n(prefix + '7_DAYS') }, { id: 7, name: i18n(prefix + '7_DAYS') },
{ id: 30, name: i18n(prefix + 'MONTH') }, { id: 30, name: i18n(prefix + 'MONTH') },
{ id: 90, name: i18n(prefix + '3_MONTHS') }, { id: 90, name: i18n(prefix + '3_MONTHS') },
{ id: 180, name: i18n(prefix + '6_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('text', self.text().trim());
append('keyword', self.keyword()); append('keyword', self.keyword());
append('in', self.selectedTreeValue()); append('in', self.selectedTreeValue());
if (-1 < self.selectedDateValue()) { if (0 < self.selectedDateValue()) {
let d = new Date(); let d = new Date();
d.setDate(d.getDate() - self.selectedDateValue()); d.setDate(d.getDate() - self.selectedDateValue());
append('since', d.toISOString().split('T')[0]); 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()); let result = decodeURIComponent(new URLSearchParams(data).toString());
@ -138,8 +151,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
self.text(pString(params.get('text'))); self.text(pString(params.get('text')));
self.keyword(pString(params.get('keyword'))); self.keyword(pString(params.get('keyword')));
self.selectedTreeValue(pString(params.get('in'))); self.selectedTreeValue(pString(params.get('in')));
// self.selectedDateValue(params.get('since')); self.selectedDateValue(0);
self.selectedDateValue(-1);
self.hasAttachment(params.has('attachment')); self.hasAttachment(params.has('attachment'));
self.starred(params.has('flagged')); self.starred(params.has('flagged'));
self.unseen(params.has('unseen')); self.unseen(params.has('unseen'));

View file

@ -15,10 +15,14 @@ export class AskPopupView extends AbstractViewPopup {
askUsername: false, askUsername: false,
passphrase: '', passphrase: '',
askPass: false, askPass: false,
remember: true, remember: true, // remember for session
askRemeber: false askRemeber: false,
rememberPermanent: false,
askRememberPermanent: false,
}); });
this.rememberPermanent.subscribe(value => value && this.remember(true));
this.fYesAction = null; this.fYesAction = null;
this.fNoAction = null; this.fNoAction = null;
@ -48,10 +52,12 @@ export class AskPopupView extends AbstractViewPopup {
this.askDesc(sAskDesc || ''); this.askDesc(sAskDesc || '');
this.askUsername(ask & 2); this.askUsername(ask & 2);
this.askPass(ask & 1); this.askPass(ask & 1);
this.askRemeber(ask & 4); this.askRemeber(ask & 4); // 0b0100
this.askRememberPermanent(ask & 0b1000)
this.username(''); this.username('');
this.passphrase(''); this.passphrase('');
this.remember(true); this.remember(true);
this.rememberPermanent(false);
this.yesButton(i18n(btnText || 'GLOBAL/YES')); this.yesButton(i18n(btnText || 'GLOBAL/YES'));
this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO')); this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
this.fYesAction = fYesFunc; this.fYesAction = fYesFunc;

View file

@ -44,6 +44,7 @@ import Remote from 'Remote/User/Fetch';
import { ComposeAttachmentModel } from 'Model/ComposeAttachment'; import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
import { EmailModel } from 'Model/Email'; import { EmailModel } from 'Model/Email';
import { IdentityModel } from 'Model/Identity';
import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt'; import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { addressparser } from 'Mime/Address'; import { addressparser } from 'Mime/Address';
@ -92,7 +93,7 @@ const
findIdentity = addresses => { findIdentity = addresses => {
addresses = addresses.map(item => item.email); 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; this.iTimer = 0;
@ -336,11 +337,14 @@ export class ComposePopupView extends AbstractViewPopup {
canMailvelope: () => this.encryptOptions.includes('Mailvelope'), canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
canSign: () => this.signOptions().length, canSign: () => this.signOptions().length,
encryptOptionsText: () => this.encryptOptions().join(', '),
signOptionsText: () => this.signOptions().map(o => o[0]).join(', '),
identitiesOptions: () => identitiesOptions: () =>
IdentityUserStore.map(item => ({ IdentityUserStore.map(item => ({
item: item, item: item,
optValue: item.id(), optValue: item.id(),
optText: item.formattedName() optText: item
})), })),
canBeSentOrSaved: () => !this.sending() && !this.saving() canBeSentOrSaved: () => !this.sending() && !this.saving()
@ -355,7 +359,7 @@ export class ComposePopupView extends AbstractViewPopup {
currentIdentity: value => { currentIdentity: value => {
if (value) { if (value) {
this.from(value.formattedName()); this.from(value.toString());
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt()); this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign()); this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
} }
@ -451,13 +455,24 @@ export class ComposePopupView extends AbstractViewPopup {
contactsCommand: self => self.allowContacts 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() { sendCommand() {
const identity = this.currentIdentity();
let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
this.attachmentsInProcessError(false); this.attachmentsInProcessError(false);
this.attachmentsInErrorError(false); this.attachmentsInErrorError(false);
this.emptyToError(false); this.emptyToError(false);
@ -475,80 +490,95 @@ export class ComposePopupView extends AbstractViewPopup {
} }
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) { if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
if (SettingsUserStore.replySameFolder()) { const sSentFolder = this.sentFolder();
if ( if ('' === sSentFolder) {
3 === arrayLength(this.aDraftInfo) &&
null != this.aDraftInfo[2] &&
this.aDraftInfo[2].length
) {
sSentFolder = this.aDraftInfo[2];
}
}
if (!sSentFolder) {
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]); showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
} else try { } else {
this.sendError(false); const sendError = e => {
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 => {
console.error(e); console.error(e);
this.sendError(true); this.sendError(true);
this.sendErrorDesc(e); this.sendErrorDesc(e);
this.sending(false); this.sending(false);
}); };
} catch (e) { const sendFailed = (iError, data) => {
console.error(e); this.sendError(true);
this.sendError(true); this.sendErrorDesc(
this.sendErrorDesc(e); getNotification(iError, data?.message, Notifications.CantSendMessage)
this.sending(false); + "\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.saveCommand();
} }
this.tryToClose(); this.doClose();
} }
contactsCommand() { contactsCommand() {
@ -833,6 +863,7 @@ export class ComposePopupView extends AbstractViewPopup {
case ComposeType.Forward: case ComposeType.Forward:
case ComposeType.ForwardAsAttachment: case ComposeType.ForwardAsAttachment:
identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc)) identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc))
|| findIdentity(oLastMessage.from)
/* || findIdentity(oLastMessage.deliveredTo)*/; /* || findIdentity(oLastMessage.deliveredTo)*/;
break; break;
case ComposeType.Draft: case ComposeType.Draft:
@ -842,10 +873,20 @@ export class ComposePopupView extends AbstractViewPopup {
// case ComposeType.Empty: // 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]; identity = identity || IdentityUserStore()[0];
if (identity) { if (identity) {
// excludeEmail.add(identity.email()); // excludeEmail.add(identity.email);
excludeEmail[identity.email()] = true; excludeEmail[identity.email] = true;
} }
if (arrayLength(options.to)) { if (arrayLength(options.to)) {
@ -861,7 +902,7 @@ export class ComposePopupView extends AbstractViewPopup {
} }
if (options.mode && oLastMessage) { if (options.mode && oLastMessage) {
let encrypted, let usePlain,
sCc = '', sCc = '',
sDate = timestampToString(oLastMessage.dateTimestamp(), 'FULL'), sDate = timestampToString(oLastMessage.dateTimestamp(), 'FULL'),
sSubject = oLastMessage.subject(), sSubject = oLastMessage.subject(),
@ -889,10 +930,9 @@ export class ComposePopupView extends AbstractViewPopup {
oLastMessage.headers().valuesByName('autocrypt').forEach(value => { oLastMessage.headers().valuesByName('autocrypt').forEach(value => {
let autocrypt = new MimeHeaderAutocryptModel(value); let autocrypt = new MimeHeaderAutocryptModel(value);
if (autocrypt.addr && autocrypt.keydata) { if (autocrypt.addr && autocrypt.keydata) {
PgpUserStore.hasPublicKeyForEmails([autocrypt.addr]).then(result => PgpUserStore.hasPublicKeyForEmails([autocrypt.addr])
result || PgpUserStore.importKey(autocrypt.pem(), true, true) || PgpUserStore.importKey(autocrypt.pem(), true, true)
// result || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()]) // || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
);
} }
}); });
} break; } break;
@ -931,7 +971,7 @@ export class ComposePopupView extends AbstractViewPopup {
// https://github.com/the-djmaze/snappymail/issues/491 // https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = oLastMessage.bodyAsHTML(); tpl.innerHTML = oLastMessage.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img => { 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(); sText = tpl.innerHTML.trim();
@ -970,18 +1010,14 @@ export class ComposePopupView extends AbstractViewPopup {
break; break;
default: default:
encrypted = PgpUserStore.isEncrypted(sText); usePlain = PgpUserStore.isEncrypted(sText) || isPlainEditor() || !oLastMessage.isHtml();
if (encrypted) { if (usePlain) {
sText = oLastMessage.plain(); sText = oLastMessage.plain();
} }
} }
this.editor(editor => { this.editor(editor => {
encrypted || editor.setHtml(sText); usePlain ? (editor.modePlain() | editor.setPlain(sText)) : editor.setHtml(sText);
if (encrypted || isPlainEditor()) {
editor.modePlain();
}
encrypted && editor.setPlain(sText);
this.setSignature(identity, options.mode); this.setSignature(identity, options.mode);
this.setFocusInPopup(); this.setFocusInPopup();
}); });
@ -1048,7 +1084,7 @@ export class ComposePopupView extends AbstractViewPopup {
}, 100); }, 100);
} }
tryToClose() { doClose() {
if (AskPopupView.hidden()) { if (AskPopupView.hidden()) {
if (ComposePopupView.inEdit() || (this.isEmptyForm() && !this.draftUid())) { if (ComposePopupView.inEdit() || (this.isEmptyForm() && !this.draftUid())) {
this.close(); this.close();
@ -1127,7 +1163,7 @@ export class ComposePopupView extends AbstractViewPopup {
.on('onComplete', (id, result, data) => { .on('onComplete', (id, result, data) => {
const attachment = this.getAttachmentById(id), const attachment = this.getAttachmentById(id),
response = data?.Result || {}, response = data?.Result || {},
errorCode = response.ErrorCode, errorCode = response.code,
attachmentJson = result && response.Attachment; attachmentJson = result && response.Attachment;
let error = ''; let error = '';
@ -1143,7 +1179,7 @@ export class ComposePopupView extends AbstractViewPopup {
.waiting(false) .waiting(false)
.uploading(false) .uploading(false)
.complete(true) .complete(true)
.error(error + '\n' + response.ErrorMessage); .error(error + '\n' + response.message);
} else if (attachmentJson) { } else if (attachmentJson) {
attachment attachment
.waiting(false) .waiting(false)
@ -1194,7 +1230,7 @@ export class ComposePopupView extends AbstractViewPopup {
}); });
addShortcut('escape,close', 'shift', ScopeCompose, () => { addShortcut('escape,close', 'shift', ScopeCompose, () => {
this.tryToClose(); this.doClose();
return false; return false;
}); });
@ -1357,7 +1393,7 @@ export class ComposePopupView extends AbstractViewPopup {
allRecipients() { allRecipients() {
return [ return [
// From/sender is also recipient (Sent mailbox) // From/sender is also recipient (Sent mailbox)
// this.currentIdentity().email(), // this.currentIdentity().email,
this.from(), this.from(),
this.to(), this.to(),
this.cc(), this.cc(),
@ -1377,7 +1413,8 @@ export class ComposePopupView extends AbstractViewPopup {
key && options.push(['OpenPGP', key]); key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1); key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]); 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']); && options.push(['S/MIME']);
console.dir({signOptions: options}); console.dir({signOptions: options});
this.signOptions(options); this.signOptions(options);
@ -1394,22 +1431,22 @@ export class ComposePopupView extends AbstractViewPopup {
OpenPGPUserStore.hasPublicKeyForEmails(recipients) OpenPGPUserStore.hasPublicKeyForEmails(recipients)
&& options.push('OpenPGP'); && options.push('OpenPGP');
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
options.push('Mailvelope');
} else {
'mailvelope' === this.viewArea() && this.bodyArea();
// this.dropMailvelope();
}
const count = recipients.length, const count = recipients.length,
identity = this.currentIdentity(), identity = this.currentIdentity(),
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null; from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email : null;
count count
&& count === recipients.filter(email => && count === recipients.filter(email =>
email == from email == from
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt) || SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
).length ).length
&& options.push('S/MIME'); && options.push('S/MIME');
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
options.push('Mailvelope');
} else {
'mailvelope' === this.viewArea() && this.bodyArea();
// this.dropMailvelope();
}
} }
console.dir({encryptOptions:options}); console.dir({encryptOptions:options});
@ -1473,12 +1510,21 @@ export class ComposePopupView extends AbstractViewPopup {
**/ **/
linkedData: [] linkedData: []
}, },
recipients = draft ? [identity.email()] : this.allRecipients(), recipients = draft ? [identity.email] : this.allRecipients(),
signOptions = !draft && this.doSign() && this.signOptions(), signOptions = !draft && this.doSign() && this.signOptions(),
encryptOptions = this.doEncrypt() && this.encryptOptions(), encryptOptions = this.doEncrypt() && this.encryptOptions(),
isHtml = this.oEditor.isHtml(); isHtml = this.oEditor.isHtml();
if (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 { do {
l = Text.length; l = Text.length;
Text = Text Text = Text
@ -1522,12 +1568,11 @@ export class ComposePopupView extends AbstractViewPopup {
alternative.children.push(data); alternative.children.push(data);
data = alternative; data = alternative;
} }
let sign = true; let isSigned = false;
for (let i = 0; i < signOptions.length; ++i) { for (let i = 0; i < signOptions.length; ++i) {
if ('OpenPGP' == signOptions[i][0]) { if ('OpenPGP' == signOptions[i][0]) {
try { try {
// Doesn't sign attachments // Doesn't sign attachments
params.html = params.plain = '';
let signed = new MimePart; let signed = new MimePart;
signed.headers['Content-Type'] = signed.headers['Content-Type'] =
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"'; '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.headers['Content-Transfer-Encoding'] = '7Bit';
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1); signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
signed.children.push(signature); signed.children.push(signature);
isSigned = true;
params.html = params.plain = '';
params.signed = signed.toString(); params.signed = signed.toString();
params.boundary = signed.boundary; params.boundary = signed.boundary;
data = signed; data = signed;
/*
Object.entries(PgpUserStore.getPublicKeyOfEmails([getEmail(this.from())]) || {})
.forEach(([k,v]) => params.publicKey = v);
*/
break;
} catch (e) { } catch (e) {
sign = false; Passphrases.delete(signOptions[i][1])
console.error(e); console.error(e);
} }
break; } else if ('GnuPG' == signOptions[i][0]) {
}
if ('GnuPG' == signOptions[i][0]) {
// TODO: sign in PHP fails // TODO: sign in PHP fails
let pass = await GnuPGUserStore.sign(signOptions[i][1]); let pass = await GnuPGUserStore.sign(signOptions[i][1]);
if (null != pass) { if (null != pass) {
// params.signData = data.toString(); // params.signData = data.toString();
params.signFingerprint = signOptions[i][1].fingerprint; params.signFingerprint = signOptions[i][1].fingerprint;
params.signPassphrase = pass; params.signPassphrase = pass;
} else { // params.attachPublicKey = false;
sign = false; isSigned = true;
break;
} }
break; } else if ('S/MIME' == signOptions[i][0]) {
}
if ('S/MIME' == signOptions[i][0]) {
// TODO: sign in PHP fails // TODO: sign in PHP fails
params.sign = 'S/MIME'; params.sign = 'S/MIME';
// params.signCertificate = identity.smimeCertificate(); // params.signCertificate = identity.smimeCertificate();
// params.signCertificateChain = identity.smimeCertificateChain();
// params.signPrivateKey = identity.smimeKey(); // params.signPrivateKey = identity.smimeKey();
// params.attachCertificate = false;
if (identity.smimeKeyEncrypted()) { if (identity.smimeKeyEncrypted()) {
const pass = await Passphrases.ask(identity, const pass = await Passphrases.ask(identity,
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}), i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
'CRYPTO/SIGN' 'CRYPTO/SIGN'
); );
if (null != pass) { if (null != pass) {
params.signPassphrase = pass.password; params.signPassphrase = pass.password;
// pass.remember && Passphrases.set(identity, pass.password); pass.remember && Passphrases.handle(identity, pass.password);
} else { isSigned = true;
sign = false;
} }
} }
} }
} }
if (signOptions.length && !sign) { if (signOptions.length && !isSigned) {
throw 'Signing failed'; throw 'Signing failed';
} }
@ -1587,7 +1637,9 @@ export class ComposePopupView extends AbstractViewPopup {
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) => Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
params.autocrypt.push({ params.autocrypt.push({
addr: k, 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) { for (let i = 0; i < encryptOptions.length; ++i) {
@ -1607,7 +1659,7 @@ export class ComposePopupView extends AbstractViewPopup {
if ('S/MIME' == encryptOptions[i]) { if ('S/MIME' == encryptOptions[i]) {
params.encryptCertificates = [identity.smimeCertificate()]; params.encryptCertificates = [identity.smimeCertificate()];
SMimeUserStore.forEach(certificate => { SMimeUserStore.forEach(certificate => {
certificate.emailAddress != identity.email() certificate.emailAddress != identity.email
&& recipients.includes(certificate.emailAddress) && recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id) && params.encryptCertificates.push(certificate.id)
}); });

Some files were not shown because too many files have changed in this diff Show more