mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-10-24 22:25:09 +03:00
Merge branch 'master' into php81
This commit is contained in:
commit
51de013007
1312 changed files with 58311 additions and 21890 deletions
|
|
@ -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; \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -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
4
.gitmodules
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
610
CHANGELOG.md
610
CHANGELOG.md
|
|
@ -1,3 +1,380 @@
|
||||||
|
## 2.38.2 – 2024-10-09
|
||||||
|
### Fixed
|
||||||
|
- error '$index is not defined' in Settings > Accounts
|
||||||
|
[#1797](https://github.com/the-djmaze/snappymail/issues/1797)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.38.1 – 2024-10-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Admin - Extensions search filter
|
||||||
|
- Options to unset current font family and size
|
||||||
|
[#1726](https://github.com/the-djmaze/snappymail/issues/1726)
|
||||||
|
- Option to allow insecure cryptkey
|
||||||
|
[#1746](https://github.com/the-djmaze/snappymail/issues/1746)
|
||||||
|
- Save vCard FN property
|
||||||
|
[#1761](https://github.com/the-djmaze/snappymail/issues/1761)
|
||||||
|
- Docker Nginx should listen on IPv6 addresses in addition to IPv4
|
||||||
|
[#1770](https://github.com/the-djmaze/snappymail/issues/1770)
|
||||||
|
- Full-screen Message View on Double-Click
|
||||||
|
[#1787](https://github.com/the-djmaze/snappymail/issues/1787)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use the custom Squire as submodule vendors/squire2
|
||||||
|
- Disallow noembed and noframes HTML elements
|
||||||
|
- Keep 1 space between HTML elements in compressed templates
|
||||||
|
- Squire: sanitizeToDOMFragment now uses cleanHTML()
|
||||||
|
- Squire: improved handling of BR elements
|
||||||
|
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
|
||||||
|
- Nextcloud border-radius by @gnilebein
|
||||||
|
[#1790](https://github.com/the-djmaze/snappymail/pull/1790)
|
||||||
|
- Compose window sentFolder handling for
|
||||||
|
[#1793](https://github.com/the-djmaze/snappymail/pull/1793)
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Portuguese (Brazil) by @mstolf
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Composer dialog scroll got broken in v2.28
|
||||||
|
- Composer dialog "from" triangle button wrong position due to font changes
|
||||||
|
- Admin - Config `search` should be ko.observable() not ko.observableArray()
|
||||||
|
- Squire: paste images
|
||||||
|
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
|
||||||
|
- Domain whitelist failures on login
|
||||||
|
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||||
|
- Sieve parse errors
|
||||||
|
- Sieve support `index` was not optional for
|
||||||
|
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
|
||||||
|
- Pagination problem for large mailbox after 2.36.1
|
||||||
|
[#1716](https://github.com/the-djmaze/snappymail/issues/1716)
|
||||||
|
- Undefined constant "LOG_ERROR"
|
||||||
|
[#1754](https://github.com/the-djmaze/snappymail/issues/1754)
|
||||||
|
- Search Filter Capital "B" not working
|
||||||
|
[#1780](https://github.com/the-djmaze/snappymail/issues/1780)
|
||||||
|
- PHP 8.4: Implicitly nullable parameter declarations deprecated
|
||||||
|
|
||||||
|
|
||||||
|
## 2.38.0 – 2024-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Remove `tel:` links when converting HTML to plain
|
||||||
|
[#1724](https://github.com/the-djmaze/snappymail/issues/1724)
|
||||||
|
- Convert HTML to Markdown instead of plain, by using [Turndown](https://github.com/mixmark-io/turndown)
|
||||||
|
[#1604](https://github.com/the-djmaze/snappymail/issues/1604)
|
||||||
|
- Check HTMLInputElement.validity() for
|
||||||
|
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use a modified [Squire 2.3.2](https://github.com/the-djmaze/Squire/commits/snappymail/)
|
||||||
|
- cleanHtml use allowedTags instead of disallowedTags and improved CSS handling
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- mXSS exploit found by SonarSource
|
||||||
|
[CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
|
||||||
|
- Call to a member function Email() on null
|
||||||
|
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||||
|
- IMAP capabilities via IMAP Proxy
|
||||||
|
[#1725](https://github.com/the-djmaze/snappymail/issues/1725)
|
||||||
|
- Messages on page setting is not validated against 999 max
|
||||||
|
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.37.3 – 2024-08-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Mark images with width=1 as tracking pixel
|
||||||
|
- Show warning in Admin -> About when PHP runs in 32bit
|
||||||
|
- Edit ACL rules
|
||||||
|
[#157](https://github.com/the-djmaze/snappymail/issues/157)
|
||||||
|
- Show GnuPG version for
|
||||||
|
[#1560](https://github.com/the-djmaze/snappymail/issues/1560)
|
||||||
|
- Make sure only scalar values are allowed in $_ENV for
|
||||||
|
[#1560](https://github.com/the-djmaze/snappymail/issues/1560)
|
||||||
|
- Change minimum new mail check interval
|
||||||
|
[#1678](https://github.com/the-djmaze/snappymail/issues/1678)
|
||||||
|
- Sieve editor does not support "index" extension
|
||||||
|
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved domain autoconfig interaction
|
||||||
|
- MS autodiscover priorities DNS over subdomain
|
||||||
|
- Simplify sieve scripts list
|
||||||
|
[#1675](https://github.com/the-djmaze/snappymail/issues/1675)
|
||||||
|
- Handling of (token) errors due to
|
||||||
|
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||||
|
- Sabre/Xml to v4.0.5
|
||||||
|
- Update Chinese by @Artoria2e5
|
||||||
|
- Update French by @hguilbert
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Thread sorting visible after disabling the imap capability
|
||||||
|
[#1574](https://github.com/the-djmaze/snappymail/issues/1574)
|
||||||
|
- Creating new message impossible as long as a draft exists?
|
||||||
|
[#1710](https://github.com/the-djmaze/snappymail/issues/1710)
|
||||||
|
- InvalidToken error at login
|
||||||
|
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Force Nextcloud personal language by default
|
||||||
|
[#1428](https://github.com/the-djmaze/snappymail/issues/1428)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.37.2 – 2024-08-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Validate Fetch JSON response
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- PATH_INFO bug due to Office365 OAuth login
|
||||||
|
- Prevent logout loop on error
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Failed loading due to Office365 OAuth2 attempt
|
||||||
|
[#1703](https://github.com/the-djmaze/snappymail/issues/1703)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.37.1 – 2024-08-12
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Gulp v5 broke the fonts
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Support v30
|
||||||
|
|
||||||
|
|
||||||
|
## 2.37.0 – 2024-08-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- JavaScript event `rl-vm-visible`
|
||||||
|
- Detailed error message on account switch failure for
|
||||||
|
[#1594](https://github.com/the-djmaze/snappymail/issues/1594)
|
||||||
|
- Workarounds for Microsoft OAuth2 (currently requires Apache AcceptPathInfo)
|
||||||
|
[#1645](https://github.com/the-djmaze/snappymail/issues/1645)
|
||||||
|
- Support "mark for deletion"
|
||||||
|
[#1657](https://github.com/the-djmaze/snappymail/issues/1657) by @smsoft-ru
|
||||||
|
- Invoke "Update Identity" pop up right after login (when not initialized)
|
||||||
|
[#1689](https://github.com/the-djmaze/snappymail/issues/1689)
|
||||||
|
- Keyboard shortcut for "Swap default (background) color"
|
||||||
|
[#1690](https://github.com/the-djmaze/snappymail/issues/1690)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated gulp to v5
|
||||||
|
- Replaced vulnerable gulp-header with gulp-append-prepend
|
||||||
|
- Removed abandoned vulnerable rollup-plugin-html
|
||||||
|
- Align save button in admin security settings
|
||||||
|
- Made registerProtocolHandler('mailto') optional by activating at Settings -> General
|
||||||
|
- Improved InvalidToken handling for
|
||||||
|
[#1653](https://github.com/the-djmaze/snappymail/issues/1653)
|
||||||
|
- Cleanup localizations
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update German by @tkasch
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- prevent multiple afterShow() and afterHide() due to `transitionend` on multiple CSS properties
|
||||||
|
- Attempt to read property "smimeSigned" on null
|
||||||
|
- Refreshing mail list doesn't update current message
|
||||||
|
[#1654](https://github.com/the-djmaze/snappymail/issues/1654)
|
||||||
|
- Deletion of CACHE folder causing error
|
||||||
|
[#1660](https://github.com/the-djmaze/snappymail/issues/1660)
|
||||||
|
- Multiple line breaks are not displayed
|
||||||
|
[#1666](https://github.com/the-djmaze/snappymail/issues/1666)
|
||||||
|
- RainLoop\Exceptions\ClientException::__construct(): Argument #2 ($oPrevious) must be of type ?Throwable, string given
|
||||||
|
[#1686](https://github.com/the-djmaze/snappymail/issues/1686)
|
||||||
|
- SpamAssassin Division by zero
|
||||||
|
[#1694](https://github.com/the-djmaze/snappymail/issues/1694)
|
||||||
|
- Failed to parse RFC 2822 date '6 Jul 2024 16:42:09 +0200'
|
||||||
|
[#1694](https://github.com/the-djmaze/snappymail/issues/1694)
|
||||||
|
- Fix capabilities when THREAD is disabled
|
||||||
|
[#1698](https://github.com/the-djmaze/snappymail/pull/1698) by @akhil1508
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Failed loading due to incorrect `app_path`
|
||||||
|
- Bugfix language detection
|
||||||
|
- Allow multi-account in nc with oauth login
|
||||||
|
[#1699](https://github.com/the-djmaze/snappymail/pull/1699) by @akhil1508
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.4 – 2024-06-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Customize private key passphrase expiration interval
|
||||||
|
[#1545](https://github.com/the-djmaze/snappymail/discussions/1545)
|
||||||
|
- AdvancedSearch support for filtering mails before a given date
|
||||||
|
[#1606](https://github.com/the-djmaze/snappymail/pull/1606) by @codiflow
|
||||||
|
- Control valid spam and virus headers
|
||||||
|
[#1607](https://github.com/the-djmaze/snappymail/issues/1607)
|
||||||
|
- Remember S/MIME private Key without function
|
||||||
|
[#1611](https://github.com/the-djmaze/snappymail/issues/1611)
|
||||||
|
- Resize compose dialog
|
||||||
|
- Magnetic theme
|
||||||
|
[#1637](https://github.com/the-djmaze/snappymail/pull/1637) by @TheCuteFoxxy
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved signing messages by allowing to choose between the options
|
||||||
|
- Improved language detection code
|
||||||
|
- More detailed Decrypt errors
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
- Update Spanish by @huloza
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Default language error
|
||||||
|
- Undefined $sEmail in DoAdminDomainMatch
|
||||||
|
- Handling Autocrypt header failed on `=`
|
||||||
|
[#1608](https://github.com/the-djmaze/snappymail/issues/1608)
|
||||||
|
- Blank lines are inserted when editing draft
|
||||||
|
[#1609](https://github.com/the-djmaze/snappymail/issues/1609)
|
||||||
|
- Workaround Cyrus MAILBOXID bug (disable OBJECTID capability by default due to impact)
|
||||||
|
[#1640](https://github.com/the-djmaze/snappymail/issues/1640)
|
||||||
|
- Workaround HTML with multiple body elements or MIME with multiple text/html
|
||||||
|
[#1641](https://github.com/the-djmaze/snappymail/issues/1641)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- OIDC stay logged in
|
||||||
|
[#1620](https://github.com/the-djmaze/snappymail/pull/1620) by @avinash-0007
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.3 – 2024-05-27
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- UserAuth prevent plugin errors (like the Nextcloud plugin did)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Undefined variable $aTokenData
|
||||||
|
[#1567](https://github.com/the-djmaze/snappymail/issues/1567)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.2 – 2024-05-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- "copy to" action in menu's for
|
||||||
|
[#1559](https://github.com/the-djmaze/snappymail/issues/1559)
|
||||||
|
- Log signal info for
|
||||||
|
[#1569](https://github.com/the-djmaze/snappymail/issues/1569)
|
||||||
|
- OpenPGP.js automatically import backup keys from server
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved "remember me" cookie handling
|
||||||
|
- Update Basque by @Thadah
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- "Account already exists"
|
||||||
|
[#1561](https://github.com/the-djmaze/snappymail/issues/1561)
|
||||||
|
- Properly escape path separator in tar.php file list regex
|
||||||
|
[#1562](https://github.com/the-djmaze/snappymail/pull/1562) by @sevmonster
|
||||||
|
- Prevent mkdir() error
|
||||||
|
[#1565](https://github.com/the-djmaze/snappymail/issues/1565)
|
||||||
|
- SCRAM Exception when trying to log in to SMTP
|
||||||
|
[#1575](https://github.com/the-djmaze/snappymail/issues/1575)
|
||||||
|
- Error when redirected back to instance after Gmail OAuth
|
||||||
|
[#1580](https://github.com/the-djmaze/snappymail/issues/1580)
|
||||||
|
- Uncaught TypeError: hasPublicKeyForEmails(...).then is not a function
|
||||||
|
[#1589](https://github.com/the-djmaze/snappymail/issues/1589)
|
||||||
|
- Undefined variable $sFilename
|
||||||
|
- GPG/PGP exec() return false handling
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- OIDC login active again
|
||||||
|
[#1572](https://github.com/the-djmaze/snappymail/pull/1572) by @avinash-0007
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.1 – 2024-04-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX
|
||||||
|
- Have I Been Pwned class to check breached passwords and email addresses
|
||||||
|
- Handle RFC 5987 in Content-Disposition header
|
||||||
|
- Ignore text/x-amp-html
|
||||||
|
- Show SMTP error to user
|
||||||
|
[#1521](https://github.com/the-djmaze/snappymail/issues/1521)
|
||||||
|
- OAuth2 for login using gmail (and others)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- logMask all AUTHENTICATE requests
|
||||||
|
- ErrorTip use white-space: pre
|
||||||
|
- Simplify LoginProcess handling
|
||||||
|
- ES2020 everywhere (require Safari 13.1)
|
||||||
|
- Modified Squire to be more in line with v2.2.8
|
||||||
|
- CSS set min-width for .attachmentParent and .flagParent to line them up
|
||||||
|
- cPanel use extension login-cpanel instead of login-remote
|
||||||
|
- Improved login credentials handling
|
||||||
|
- Speedup Knockout a bit and removed `with($context)` scope
|
||||||
|
- Update Belarusian by @spoooyders
|
||||||
|
- Update Chinese by @mayswind
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Content encoding and type detection in JavaScript could fail due to case-sensitivity.
|
||||||
|
- Extensions set logger failed
|
||||||
|
- GnuPG check open_basedir and if shell_exec is disabled
|
||||||
|
[#1385](https://github.com/the-djmaze/snappymail/issues/1385)
|
||||||
|
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
|
||||||
|
[#1555](https://github.com/the-djmaze/snappymail/issues/1555)
|
||||||
|
- Hide pagination when search result has no messages
|
||||||
|
- Prevent mbstring error before setup.php
|
||||||
|
- Prevent MessagesPerPage Infinity
|
||||||
|
[#1540](https://github.com/the-djmaze/snappymail/issues/1540)
|
||||||
|
- Reseal CryptKey failed
|
||||||
|
[#1543](https://github.com/the-djmaze/snappymail/issues/1543)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Add an occ command to set up the login settings
|
||||||
|
[#1552](https://github.com/the-djmaze/snappymail/issues/1552)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.36.0 – 2024-03-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Allow setting the supported THREAD algorithm
|
||||||
|
- Icon to system folders
|
||||||
|
- Remove remembered password after 15 minutes of inactivity
|
||||||
|
[#1142](https://github.com/the-djmaze/snappymail/issues/1142)
|
||||||
|
- Swap background and text color for unreadable text on dark background
|
||||||
|
[#1486](https://github.com/the-djmaze/snappymail/issues/1486)
|
||||||
|
- Generate TOTP code at ?Admin -> Security
|
||||||
|
[#1501](https://github.com/the-djmaze/snappymail/issues/1501)
|
||||||
|
- Button to change S/MIME private key passphrase
|
||||||
|
[#1505](https://github.com/the-djmaze/snappymail/issues/1505)
|
||||||
|
- Belarusian
|
||||||
|
[#1512](https://github.com/the-djmaze/snappymail/pull/1512) by @spoooyders
|
||||||
|
- Log some domain idn_to_ascii issues
|
||||||
|
[#1513](https://github.com/the-djmaze/snappymail/issues/1513)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- On folder/mailbox rename, also rename all children instead of reloading all
|
||||||
|
- Seal MainAccount CryptKey and on error ask old login passphrase to reseal key.
|
||||||
|
- Moved cache drivers outside core into extensions
|
||||||
|
- Sieve always allow fileinto INBOX
|
||||||
|
[#1510](https://github.com/the-djmaze/snappymail/issues/1510)
|
||||||
|
- Moved application.ini `sieve_auth_plain_initial` to per domain config
|
||||||
|
- Languages use rfc5646, by using the shortest ISO 639 code by default
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- On folder/mailbox rename, the old fullName must be removed from cache
|
||||||
|
- On folder/mailbox rename, the checkable option was not renamed
|
||||||
|
- Sort accounts drag & drop
|
||||||
|
- S/MIME encrypted and opaque signed not visible
|
||||||
|
[#1450](https://github.com/the-djmaze/snappymail/issues/1450)
|
||||||
|
- Wrong last UID of thread
|
||||||
|
[#1507](https://github.com/the-djmaze/snappymail/issues/1507)
|
||||||
|
- Creation of dynamic property SnappyMail\DAV\Client::$HTTP
|
||||||
|
[#1509](https://github.com/the-djmaze/snappymail/issues/1509)
|
||||||
|
- "Download as ZIP" fails for messages
|
||||||
|
[#1514](https://github.com/the-djmaze/snappymail/issues/1514)
|
||||||
|
- SMTP "Authentication failed" when IMAP uses `shortLogin` and SMTP not
|
||||||
|
[#1517](https://github.com/the-djmaze/snappymail/issues/1517)
|
||||||
|
|
||||||
|
|
||||||
## 2.35.4 – 2024-03-16
|
## 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 ` ` for
|
- Squire space handling on paste use `\u00A0` instead of ` ` 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
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -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 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
"'": '''
|
"'": '''
|
||||||
},
|
},
|
||||||
|
|
||||||
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)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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')),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
115
dev/External/SquireUI.js
vendored
115
dev/External/SquireUI.js
vendored
|
|
@ -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});
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
dev/External/User/ko.js
vendored
2
dev/External/User/ko.js
vendored
|
|
@ -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
7
dev/External/ko.js
vendored
|
|
@ -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] });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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?
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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('@'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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})` : '');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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) : ''
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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() : ""))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
+ ';';
|
+ ';';
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 : ';'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,9 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
grid-column-end: 3;
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#V-PopupsLanguages {
|
#V-PopupsLanguages {
|
||||||
|
|
||||||
max-width: 710px;
|
max-width: 720px;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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: '☆'; /*⚐*/
|
||||||
|
|
|
||||||
|
|
@ -45,4 +45,8 @@
|
||||||
content: ')';
|
content: ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.identities-list tr + tr .identity-default {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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] {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue