mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-10-24 22:25:09 +03:00
Compare commits
No commits in common. "51de013007a7daefe86f21aa66428e20cbec11ef" and "ef993aae149b1c636cf6bbdd6efc4ea14d93ff2f" have entirely different histories.
51de013007
...
ef993aae14
1310 changed files with 21846 additions and 58267 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM alpine:3.24.3 AS builder
|
||||
RUN apk add --no-cache php85 php85-json php-phar php-zip
|
||||
FROM alpine:3.18.5 AS builder
|
||||
RUN apk add --no-cache php82 php82-json php-phar php-zip
|
||||
RUN apk add --no-cache npm
|
||||
RUN npm install -g gulp yarn
|
||||
WORKDIR /source
|
||||
|
|
@ -23,7 +23,7 @@ RUN set -eux; \
|
|||
rm -v /snappymail/README.md /snappymail/_include.php
|
||||
|
||||
# Inspired by the original Rainloop dockerfile from youtous on GitLab
|
||||
FROM php:8.5-fpm-alpine AS final
|
||||
FROM php:8.2-fpm-alpine AS final
|
||||
|
||||
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
|
||||
|
||||
|
|
@ -99,6 +99,9 @@ RUN set -eux; \
|
|||
# mysql
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
|
||||
# opcache
|
||||
RUN docker-php-ext-install opcache
|
||||
|
||||
# postgres
|
||||
RUN set -eux; \
|
||||
apk add --no-cache postgresql-libs; \
|
||||
|
|
@ -119,13 +122,6 @@ RUN set -eux; \
|
|||
apk del .build-dependencies; \
|
||||
apk del .deps
|
||||
|
||||
# sqlite
|
||||
RUN set -eux; \
|
||||
apk add --no-cache sqlite-libs; \
|
||||
apk add --no-cache --virtual .deps sqlite-dev; \
|
||||
docker-php-ext-install pdo_sqlite; \
|
||||
apk del .deps
|
||||
|
||||
# tidy
|
||||
RUN set -eux; \
|
||||
apk add --no-cache tidyhtml; \
|
||||
|
|
|
|||
|
|
@ -9,12 +9,6 @@ UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
|
|||
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
|
||||
SECURE_COOKIES=${SECURE_COOKIES:-true}
|
||||
|
||||
# Disable listening on ipv6 for ipv4-only environments, or nginx will fail to start
|
||||
if ! ip a | grep 'inet6 ' > /dev/null; then
|
||||
echo "[INFO] ipv6 is disabled. Configuring nginx to not listen on ipv6"
|
||||
sed -i '/listen \[::\]:/d' /etc/nginx/nginx.conf
|
||||
fi
|
||||
|
||||
# Set attachment size limit
|
||||
sed -i "s/<UPLOAD_MAX_SIZE>/$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
|
||||
|
|
|
|||
|
|
@ -52,8 +52,6 @@ http {
|
|||
|
||||
server {
|
||||
listen 8888;
|
||||
listen [::]:8888;
|
||||
#listen [::]:8888 ipv6only=off;
|
||||
root /snappymail;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ module.exports = {
|
|||
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||
extends: ['eslint:recommended'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 11,
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module'
|
||||
},
|
||||
env: {
|
||||
|
|
@ -37,11 +37,7 @@ module.exports = {
|
|||
// Mailvelope
|
||||
'mailvelope': "readonly",
|
||||
// Punycode
|
||||
'IDN': "readonly",
|
||||
// Turndown
|
||||
'TurndownService': "readonly",
|
||||
// Marked
|
||||
'marked': "readonly"
|
||||
'IDN': "readonly"
|
||||
},
|
||||
// http://eslint.org/docs/rules/
|
||||
rules: {
|
||||
|
|
|
|||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -30,9 +30,8 @@ If applicable, add screenshots to help explain your problem.
|
|||
- SnappyMail Version:
|
||||
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
|
||||
|
||||
**Debug/logging information**
|
||||
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
|
||||
- [ ] I've placed them here (few lines) or as attachments (many lines)
|
||||
**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)**
|
||||
Place them here (few lines) or as attachments (many lines)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -25,8 +25,6 @@
|
|||
/vendors/knockout/spec
|
||||
/vendors/openpgp-5
|
||||
!/vendors/openpgp-5/dist
|
||||
/vendors/squire2
|
||||
!/vendors/squire2/dist
|
||||
/vendors/vanillaqr.js/
|
||||
/integrations/nextcloud/rainloop
|
||||
/integrations/owncloud/rainloop
|
||||
|
|
|
|||
4
.gitmodules
vendored
4
.gitmodules
vendored
|
|
@ -1,7 +1,3 @@
|
|||
[submodule "vendors/openpgp-5"]
|
||||
path = vendors/openpgp-5
|
||||
url = git@github.com:the-djmaze/openpgpjs.git
|
||||
[submodule "vendors/squire2"]
|
||||
path = vendors/squire2
|
||||
url = git@github.com:the-djmaze/Squire.git
|
||||
branch = snappymail
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
<FilesMatch "index\.php">
|
||||
AcceptPathInfo On
|
||||
</FilesMatch>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
# Redirect cPanel
|
||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
|
|
|
|||
610
CHANGELOG.md
610
CHANGELOG.md
|
|
@ -1,380 +1,3 @@
|
|||
## 2.38.2 – 2024-10-09
|
||||
### Fixed
|
||||
- error '$index is not defined' in Settings > Accounts
|
||||
[#1797](https://github.com/the-djmaze/snappymail/issues/1797)
|
||||
|
||||
|
||||
## 2.38.1 – 2024-10-08
|
||||
|
||||
### Added
|
||||
- Admin - Extensions search filter
|
||||
- Options to unset current font family and size
|
||||
[#1726](https://github.com/the-djmaze/snappymail/issues/1726)
|
||||
- Option to allow insecure cryptkey
|
||||
[#1746](https://github.com/the-djmaze/snappymail/issues/1746)
|
||||
- Save vCard FN property
|
||||
[#1761](https://github.com/the-djmaze/snappymail/issues/1761)
|
||||
- Docker Nginx should listen on IPv6 addresses in addition to IPv4
|
||||
[#1770](https://github.com/the-djmaze/snappymail/issues/1770)
|
||||
- Full-screen Message View on Double-Click
|
||||
[#1787](https://github.com/the-djmaze/snappymail/issues/1787)
|
||||
|
||||
### Changed
|
||||
- Use the custom Squire as submodule vendors/squire2
|
||||
- Disallow noembed and noframes HTML elements
|
||||
- Keep 1 space between HTML elements in compressed templates
|
||||
- Squire: sanitizeToDOMFragment now uses cleanHTML()
|
||||
- Squire: improved handling of BR elements
|
||||
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
|
||||
- Nextcloud border-radius by @gnilebein
|
||||
[#1790](https://github.com/the-djmaze/snappymail/pull/1790)
|
||||
- Compose window sentFolder handling for
|
||||
[#1793](https://github.com/the-djmaze/snappymail/pull/1793)
|
||||
- Update Portuguese by @ner00
|
||||
- Update Polish by @tinola
|
||||
- Update French by @hguilbert
|
||||
- Update Portuguese (Brazil) by @mstolf
|
||||
|
||||
### Fixed
|
||||
- Composer dialog scroll got broken in v2.28
|
||||
- Composer dialog "from" triangle button wrong position due to font changes
|
||||
- Admin - Config `search` should be ko.observable() not ko.observableArray()
|
||||
- Squire: paste images
|
||||
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
|
||||
- Domain whitelist failures on login
|
||||
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||
- Sieve parse errors
|
||||
- Sieve support `index` was not optional for
|
||||
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
|
||||
- Pagination problem for large mailbox after 2.36.1
|
||||
[#1716](https://github.com/the-djmaze/snappymail/issues/1716)
|
||||
- Undefined constant "LOG_ERROR"
|
||||
[#1754](https://github.com/the-djmaze/snappymail/issues/1754)
|
||||
- Search Filter Capital "B" not working
|
||||
[#1780](https://github.com/the-djmaze/snappymail/issues/1780)
|
||||
- PHP 8.4: Implicitly nullable parameter declarations deprecated
|
||||
|
||||
|
||||
## 2.38.0 – 2024-09-16
|
||||
|
||||
### Added
|
||||
- Remove `tel:` links when converting HTML to plain
|
||||
[#1724](https://github.com/the-djmaze/snappymail/issues/1724)
|
||||
- Convert HTML to Markdown instead of plain, by using [Turndown](https://github.com/mixmark-io/turndown)
|
||||
[#1604](https://github.com/the-djmaze/snappymail/issues/1604)
|
||||
- Check HTMLInputElement.validity() for
|
||||
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
|
||||
|
||||
### Changed
|
||||
- Use a modified [Squire 2.3.2](https://github.com/the-djmaze/Squire/commits/snappymail/)
|
||||
- cleanHtml use allowedTags instead of disallowedTags and improved CSS handling
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
### Fixed
|
||||
- mXSS exploit found by SonarSource
|
||||
[CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
|
||||
- Call to a member function Email() on null
|
||||
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||
- IMAP capabilities via IMAP Proxy
|
||||
[#1725](https://github.com/the-djmaze/snappymail/issues/1725)
|
||||
- Messages on page setting is not validated against 999 max
|
||||
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
|
||||
|
||||
|
||||
## 2.37.3 – 2024-08-27
|
||||
|
||||
### Added
|
||||
- Mark images with width=1 as tracking pixel
|
||||
- Show warning in Admin -> About when PHP runs in 32bit
|
||||
- Edit ACL rules
|
||||
[#157](https://github.com/the-djmaze/snappymail/issues/157)
|
||||
- Show GnuPG version for
|
||||
[#1560](https://github.com/the-djmaze/snappymail/issues/1560)
|
||||
- Make sure only scalar values are allowed in $_ENV for
|
||||
[#1560](https://github.com/the-djmaze/snappymail/issues/1560)
|
||||
- Change minimum new mail check interval
|
||||
[#1678](https://github.com/the-djmaze/snappymail/issues/1678)
|
||||
- Sieve editor does not support "index" extension
|
||||
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
|
||||
|
||||
### Changed
|
||||
- Improved domain autoconfig interaction
|
||||
- MS autodiscover priorities DNS over subdomain
|
||||
- Simplify sieve scripts list
|
||||
[#1675](https://github.com/the-djmaze/snappymail/issues/1675)
|
||||
- Handling of (token) errors due to
|
||||
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||
- Sabre/Xml to v4.0.5
|
||||
- Update Chinese by @Artoria2e5
|
||||
- Update French by @hguilbert
|
||||
|
||||
### Fixed
|
||||
- Thread sorting visible after disabling the imap capability
|
||||
[#1574](https://github.com/the-djmaze/snappymail/issues/1574)
|
||||
- Creating new message impossible as long as a draft exists?
|
||||
[#1710](https://github.com/the-djmaze/snappymail/issues/1710)
|
||||
- InvalidToken error at login
|
||||
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||
|
||||
### Nextcloud
|
||||
- Force Nextcloud personal language by default
|
||||
[#1428](https://github.com/the-djmaze/snappymail/issues/1428)
|
||||
|
||||
|
||||
## 2.37.2 – 2024-08-13
|
||||
|
||||
### Added
|
||||
- Validate Fetch JSON response
|
||||
|
||||
### Fixed
|
||||
- PATH_INFO bug due to Office365 OAuth login
|
||||
- Prevent logout loop on error
|
||||
|
||||
### Nextcloud
|
||||
- Failed loading due to Office365 OAuth2 attempt
|
||||
[#1703](https://github.com/the-djmaze/snappymail/issues/1703)
|
||||
|
||||
|
||||
## 2.37.1 – 2024-08-12
|
||||
|
||||
### Fixed
|
||||
- Gulp v5 broke the fonts
|
||||
|
||||
### Nextcloud
|
||||
- Support v30
|
||||
|
||||
|
||||
## 2.37.0 – 2024-08-12
|
||||
|
||||
### Added
|
||||
- JavaScript event `rl-vm-visible`
|
||||
- Detailed error message on account switch failure for
|
||||
[#1594](https://github.com/the-djmaze/snappymail/issues/1594)
|
||||
- Workarounds for Microsoft OAuth2 (currently requires Apache AcceptPathInfo)
|
||||
[#1645](https://github.com/the-djmaze/snappymail/issues/1645)
|
||||
- Support "mark for deletion"
|
||||
[#1657](https://github.com/the-djmaze/snappymail/issues/1657) by @smsoft-ru
|
||||
- Invoke "Update Identity" pop up right after login (when not initialized)
|
||||
[#1689](https://github.com/the-djmaze/snappymail/issues/1689)
|
||||
- Keyboard shortcut for "Swap default (background) color"
|
||||
[#1690](https://github.com/the-djmaze/snappymail/issues/1690)
|
||||
|
||||
### Changed
|
||||
- Updated gulp to v5
|
||||
- Replaced vulnerable gulp-header with gulp-append-prepend
|
||||
- Removed abandoned vulnerable rollup-plugin-html
|
||||
- Align save button in admin security settings
|
||||
- Made registerProtocolHandler('mailto') optional by activating at Settings -> General
|
||||
- Improved InvalidToken handling for
|
||||
[#1653](https://github.com/the-djmaze/snappymail/issues/1653)
|
||||
- Cleanup localizations
|
||||
- Update French by @hguilbert
|
||||
- Update German by @tkasch
|
||||
- Update Polish by @tinola
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
### Fixed
|
||||
- prevent multiple afterShow() and afterHide() due to `transitionend` on multiple CSS properties
|
||||
- Attempt to read property "smimeSigned" on null
|
||||
- Refreshing mail list doesn't update current message
|
||||
[#1654](https://github.com/the-djmaze/snappymail/issues/1654)
|
||||
- Deletion of CACHE folder causing error
|
||||
[#1660](https://github.com/the-djmaze/snappymail/issues/1660)
|
||||
- Multiple line breaks are not displayed
|
||||
[#1666](https://github.com/the-djmaze/snappymail/issues/1666)
|
||||
- RainLoop\Exceptions\ClientException::__construct(): Argument #2 ($oPrevious) must be of type ?Throwable, string given
|
||||
[#1686](https://github.com/the-djmaze/snappymail/issues/1686)
|
||||
- SpamAssassin Division by zero
|
||||
[#1694](https://github.com/the-djmaze/snappymail/issues/1694)
|
||||
- Failed to parse RFC 2822 date '6 Jul 2024 16:42:09 +0200'
|
||||
[#1694](https://github.com/the-djmaze/snappymail/issues/1694)
|
||||
- Fix capabilities when THREAD is disabled
|
||||
[#1698](https://github.com/the-djmaze/snappymail/pull/1698) by @akhil1508
|
||||
|
||||
### Nextcloud
|
||||
- Failed loading due to incorrect `app_path`
|
||||
- Bugfix language detection
|
||||
- Allow multi-account in nc with oauth login
|
||||
[#1699](https://github.com/the-djmaze/snappymail/pull/1699) by @akhil1508
|
||||
|
||||
|
||||
## 2.36.4 – 2024-06-25
|
||||
|
||||
### Added
|
||||
- Customize private key passphrase expiration interval
|
||||
[#1545](https://github.com/the-djmaze/snappymail/discussions/1545)
|
||||
- AdvancedSearch support for filtering mails before a given date
|
||||
[#1606](https://github.com/the-djmaze/snappymail/pull/1606) by @codiflow
|
||||
- Control valid spam and virus headers
|
||||
[#1607](https://github.com/the-djmaze/snappymail/issues/1607)
|
||||
- Remember S/MIME private Key without function
|
||||
[#1611](https://github.com/the-djmaze/snappymail/issues/1611)
|
||||
- Resize compose dialog
|
||||
- Magnetic theme
|
||||
[#1637](https://github.com/the-djmaze/snappymail/pull/1637) by @TheCuteFoxxy
|
||||
|
||||
### Changed
|
||||
- Improved signing messages by allowing to choose between the options
|
||||
- Improved language detection code
|
||||
- More detailed Decrypt errors
|
||||
- Update French by @hguilbert
|
||||
- Update Polish by @tinola
|
||||
- Update Portuguese by @ner00
|
||||
- Update Spanish by @huloza
|
||||
|
||||
### Fixed
|
||||
- Default language error
|
||||
- Undefined $sEmail in DoAdminDomainMatch
|
||||
- Handling Autocrypt header failed on `=`
|
||||
[#1608](https://github.com/the-djmaze/snappymail/issues/1608)
|
||||
- Blank lines are inserted when editing draft
|
||||
[#1609](https://github.com/the-djmaze/snappymail/issues/1609)
|
||||
- Workaround Cyrus MAILBOXID bug (disable OBJECTID capability by default due to impact)
|
||||
[#1640](https://github.com/the-djmaze/snappymail/issues/1640)
|
||||
- Workaround HTML with multiple body elements or MIME with multiple text/html
|
||||
[#1641](https://github.com/the-djmaze/snappymail/issues/1641)
|
||||
|
||||
### Nextcloud
|
||||
- OIDC stay logged in
|
||||
[#1620](https://github.com/the-djmaze/snappymail/pull/1620) by @avinash-0007
|
||||
|
||||
|
||||
## 2.36.3 – 2024-05-27
|
||||
|
||||
### Changed
|
||||
- UserAuth prevent plugin errors (like the Nextcloud plugin did)
|
||||
|
||||
### Fixed
|
||||
- Undefined variable $aTokenData
|
||||
[#1567](https://github.com/the-djmaze/snappymail/issues/1567)
|
||||
|
||||
|
||||
## 2.36.2 – 2024-05-26
|
||||
|
||||
### Added
|
||||
- "copy to" action in menu's for
|
||||
[#1559](https://github.com/the-djmaze/snappymail/issues/1559)
|
||||
- Log signal info for
|
||||
[#1569](https://github.com/the-djmaze/snappymail/issues/1569)
|
||||
- OpenPGP.js automatically import backup keys from server
|
||||
|
||||
### Changed
|
||||
- Improved "remember me" cookie handling
|
||||
- Update Basque by @Thadah
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
### Fixed
|
||||
- "Account already exists"
|
||||
[#1561](https://github.com/the-djmaze/snappymail/issues/1561)
|
||||
- Properly escape path separator in tar.php file list regex
|
||||
[#1562](https://github.com/the-djmaze/snappymail/pull/1562) by @sevmonster
|
||||
- Prevent mkdir() error
|
||||
[#1565](https://github.com/the-djmaze/snappymail/issues/1565)
|
||||
- SCRAM Exception when trying to log in to SMTP
|
||||
[#1575](https://github.com/the-djmaze/snappymail/issues/1575)
|
||||
- Error when redirected back to instance after Gmail OAuth
|
||||
[#1580](https://github.com/the-djmaze/snappymail/issues/1580)
|
||||
- Uncaught TypeError: hasPublicKeyForEmails(...).then is not a function
|
||||
[#1589](https://github.com/the-djmaze/snappymail/issues/1589)
|
||||
- Undefined variable $sFilename
|
||||
- GPG/PGP exec() return false handling
|
||||
|
||||
### Nextcloud
|
||||
- OIDC login active again
|
||||
[#1572](https://github.com/the-djmaze/snappymail/pull/1572) by @avinash-0007
|
||||
|
||||
|
||||
## 2.36.1 – 2024-04-23
|
||||
|
||||
### Added
|
||||
- Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX
|
||||
- Have I Been Pwned class to check breached passwords and email addresses
|
||||
- Handle RFC 5987 in Content-Disposition header
|
||||
- Ignore text/x-amp-html
|
||||
- Show SMTP error to user
|
||||
[#1521](https://github.com/the-djmaze/snappymail/issues/1521)
|
||||
- OAuth2 for login using gmail (and others)
|
||||
|
||||
### Changed
|
||||
- logMask all AUTHENTICATE requests
|
||||
- ErrorTip use white-space: pre
|
||||
- Simplify LoginProcess handling
|
||||
- ES2020 everywhere (require Safari 13.1)
|
||||
- Modified Squire to be more in line with v2.2.8
|
||||
- CSS set min-width for .attachmentParent and .flagParent to line them up
|
||||
- cPanel use extension login-cpanel instead of login-remote
|
||||
- Improved login credentials handling
|
||||
- Speedup Knockout a bit and removed `with($context)` scope
|
||||
- Update Belarusian by @spoooyders
|
||||
- Update Chinese by @mayswind
|
||||
- Update French by @hguilbert
|
||||
- Update Polish by @tinola
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
### Fixed
|
||||
- Content encoding and type detection in JavaScript could fail due to case-sensitivity.
|
||||
- Extensions set logger failed
|
||||
- GnuPG check open_basedir and if shell_exec is disabled
|
||||
[#1385](https://github.com/the-djmaze/snappymail/issues/1385)
|
||||
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
|
||||
[#1555](https://github.com/the-djmaze/snappymail/issues/1555)
|
||||
- Hide pagination when search result has no messages
|
||||
- Prevent mbstring error before setup.php
|
||||
- Prevent MessagesPerPage Infinity
|
||||
[#1540](https://github.com/the-djmaze/snappymail/issues/1540)
|
||||
- Reseal CryptKey failed
|
||||
[#1543](https://github.com/the-djmaze/snappymail/issues/1543)
|
||||
|
||||
### Nextcloud
|
||||
- Add an occ command to set up the login settings
|
||||
[#1552](https://github.com/the-djmaze/snappymail/issues/1552)
|
||||
|
||||
|
||||
## 2.36.0 – 2024-03-18
|
||||
|
||||
### Added
|
||||
- Allow setting the supported THREAD algorithm
|
||||
- Icon to system folders
|
||||
- Remove remembered password after 15 minutes of inactivity
|
||||
[#1142](https://github.com/the-djmaze/snappymail/issues/1142)
|
||||
- Swap background and text color for unreadable text on dark background
|
||||
[#1486](https://github.com/the-djmaze/snappymail/issues/1486)
|
||||
- Generate TOTP code at ?Admin -> Security
|
||||
[#1501](https://github.com/the-djmaze/snappymail/issues/1501)
|
||||
- Button to change S/MIME private key passphrase
|
||||
[#1505](https://github.com/the-djmaze/snappymail/issues/1505)
|
||||
- Belarusian
|
||||
[#1512](https://github.com/the-djmaze/snappymail/pull/1512) by @spoooyders
|
||||
- Log some domain idn_to_ascii issues
|
||||
[#1513](https://github.com/the-djmaze/snappymail/issues/1513)
|
||||
|
||||
### Changed
|
||||
- On folder/mailbox rename, also rename all children instead of reloading all
|
||||
- Seal MainAccount CryptKey and on error ask old login passphrase to reseal key.
|
||||
- Moved cache drivers outside core into extensions
|
||||
- Sieve always allow fileinto INBOX
|
||||
[#1510](https://github.com/the-djmaze/snappymail/issues/1510)
|
||||
- Moved application.ini `sieve_auth_plain_initial` to per domain config
|
||||
- Languages use rfc5646, by using the shortest ISO 639 code by default
|
||||
- Update French by @hguilbert
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
### Fixed
|
||||
- On folder/mailbox rename, the old fullName must be removed from cache
|
||||
- On folder/mailbox rename, the checkable option was not renamed
|
||||
- Sort accounts drag & drop
|
||||
- S/MIME encrypted and opaque signed not visible
|
||||
[#1450](https://github.com/the-djmaze/snappymail/issues/1450)
|
||||
- Wrong last UID of thread
|
||||
[#1507](https://github.com/the-djmaze/snappymail/issues/1507)
|
||||
- Creation of dynamic property SnappyMail\DAV\Client::$HTTP
|
||||
[#1509](https://github.com/the-djmaze/snappymail/issues/1509)
|
||||
- "Download as ZIP" fails for messages
|
||||
[#1514](https://github.com/the-djmaze/snappymail/issues/1514)
|
||||
- SMTP "Authentication failed" when IMAP uses `shortLogin` and SMTP not
|
||||
[#1517](https://github.com/the-djmaze/snappymail/issues/1517)
|
||||
|
||||
|
||||
## 2.35.4 – 2024-03-16
|
||||
|
||||
### Added
|
||||
|
|
@ -383,6 +6,7 @@
|
|||
### Changed
|
||||
- OpenPGP.js to v5.11.1
|
||||
- punycode.js lowercase domain names
|
||||
- Nextcloud changed stored password handling
|
||||
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
|
||||
- Update Portuguese by @ner00
|
||||
|
||||
|
|
@ -397,11 +21,6 @@
|
|||
[#1500](https://github.com/the-djmaze/snappymail/issues/1500)
|
||||
- VCard PHP Notice: Undefined index: ENCODING
|
||||
|
||||
### Nextcloud
|
||||
- Changed stored password handling
|
||||
- Can't login from nextcloud with 2.35.3 bug Nextcloud
|
||||
[#1490](https://github.com/the-djmaze/snappymail/issues/1490)
|
||||
|
||||
|
||||
## 2.35.3 – 2024-03-12
|
||||
|
||||
|
|
@ -449,9 +68,8 @@
|
|||
- Crash on importing corrupt OpenPGP keys
|
||||
- Crash on old browsers instead of showing error
|
||||
- Ignore popups on logoutReload()
|
||||
[#1467](https://github.com/the-djmaze/snappymail/issues/1467)
|
||||
- Custom SASLMechanisms fail in IMAP when the connection is secure
|
||||
[#1484](https://github.com/the-djmaze/snappymail/pull/1484) by @botsarenthuman
|
||||
[#1484](https://github.com/the-djmaze/snappymail/pull/1484)
|
||||
|
||||
|
||||
## 2.35.2 – 2024-02-27
|
||||
|
|
@ -468,9 +86,7 @@
|
|||
- Drop support for gnupg PECL extension as it fails with "no passphrase" issues
|
||||
- Error 352 when getting Filters
|
||||
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
|
||||
|
||||
### Nextcloud
|
||||
- SetPassword(): Argument #1 must be of type SensitiveString, string given
|
||||
- Nextcloud SetPassword(): Argument #1 must be of type SensitiveString, string given
|
||||
[#1456](https://github.com/the-djmaze/snappymail/issues/1456)
|
||||
|
||||
|
||||
|
|
@ -651,8 +267,8 @@
|
|||
- Run full GetUids() in background when message_list_limit is set
|
||||
- MessageListThreadsMap as background task when message_list_limit is set
|
||||
- Properly set CACHEDIR.TAG
|
||||
- Sending group email to all contact addresses
|
||||
[#1286](https://github.com/the-djmaze/snappymail/pull/1286) by @rezaei92
|
||||
- Sending group email to all contact addresses by @rezaei92
|
||||
[#1286](https://github.com/the-djmaze/snappymail/pull/1286)
|
||||
|
||||
### Changed
|
||||
- Default IMAP message_list_limit to 10000
|
||||
|
|
@ -660,8 +276,8 @@
|
|||
- Improved Squire WYSIWYG
|
||||
- Sort real attachments and inline attachments for
|
||||
[#1360](https://github.com/the-djmaze/snappymail/issues/1360)
|
||||
- Nextcloud Theme fixes and improvements
|
||||
[#1363](https://github.com/the-djmaze/snappymail/pull/1363) by @hampoelz
|
||||
- Nextcloud Theme fixes and improvements by @hampoelz
|
||||
[#1363](https://github.com/the-djmaze/snappymail/pull/1363)
|
||||
- Improve display of attachments
|
||||
[#1361](https://github.com/the-djmaze/snappymail/issues/1361)
|
||||
- Rename messageVisibility to messageVisible
|
||||
|
|
@ -675,8 +291,8 @@
|
|||
- Preview tooltip shows "null" when PREVIEW capability is disabled
|
||||
|
||||
### Nextcloud
|
||||
- Improved language handling
|
||||
[#1362](https://github.com/the-djmaze/snappymail/pull/1362) by @avinash-0007
|
||||
- Improved language handling by @avinash-0007
|
||||
[#1362](https://github.com/the-djmaze/snappymail/pull/1362)
|
||||
- FilterLanguage had wrong parameter order
|
||||
- Use NextcloudV25+ theme by default
|
||||
|
||||
|
|
@ -697,8 +313,8 @@
|
|||
[#1344](https://github.com/the-djmaze/snappymail/issues/1344)
|
||||
- NO Mailbox does not exist, or must be subscribed to")
|
||||
[#1354](https://github.com/the-djmaze/snappymail/issues/1354)
|
||||
- Flag indicators are added to wrong message
|
||||
[#1347](https://github.com/the-djmaze/snappymail/pull/1347) by @SergeyMosin
|
||||
- Flag indicators are added to wrong message by @SergeyMosin
|
||||
[#1347](https://github.com/the-djmaze/snappymail/pull/1347)
|
||||
- Squire: issue when using the enter key in a reply window
|
||||
[#1296](https://github.com/the-djmaze/snappymail/issues/1296)
|
||||
- Squire: crash on cut/delete range
|
||||
|
|
@ -812,8 +428,8 @@
|
|||
[#1308](https://github.com/the-djmaze/snappymail/issues/1308)
|
||||
- With email open, "delete" doesn't delete #1274
|
||||
[#1274](https://github.com/the-djmaze/snappymail/issues/1274)
|
||||
- Fix threading view in Thunderbird (others?)
|
||||
[#1304](https://github.com/the-djmaze/snappymail/pull/1304) by @tkasch
|
||||
- Fix threading view in Thunderbird (others?) by @tkasch
|
||||
[#1304](https://github.com/the-djmaze/snappymail/pull/1304)
|
||||
|
||||
|
||||
## 2.29.1 – 2023-10-02
|
||||
|
|
@ -827,18 +443,18 @@
|
|||
### Added
|
||||
- Modern UI / Nextcloud Theme
|
||||
[#629](https://github.com/the-djmaze/snappymail/pull/629) by @hampoelz
|
||||
- "Add/Edit signature" label to PopupsIdentity.html
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- "Add/Edit signature" label to PopupsIdentity.html by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- use calendar icon in message list for messages with '.ics' or 'text/calendar' attachments by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- Show unseen message count when the message list is threaded
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- in mobile mode hide folders(left) panel when a folder is clicked
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- spellcheck the subject when 'allowSpellcheck' setting is true
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section)
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- Show unseen message count when the message list is threaded by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- in mobile mode hide folders(left) panel when a folder is clicked by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- spellcheck the subject when 'allowSpellcheck' setting is true by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section) by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- Browser support for autocompleting TOTP code
|
||||
[#1251](https://github.com/the-djmaze/snappymail/issues/1251)
|
||||
|
||||
|
|
@ -849,10 +465,10 @@
|
|||
[#1199](https://github.com/the-djmaze/snappymail/issues/1199)
|
||||
- Draft code to improve mobile breakpoints
|
||||
[#1150](https://github.com/the-djmaze/snappymail/issues/1150)
|
||||
- address input: space character can trigger '_parseValue' if the email address looks complete
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- address input: space character can trigger '_parseValue' if the email address looks complete by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
|
||||
### Fixed
|
||||
- CHARSET is not valid in UTF8 mode
|
||||
|
|
@ -865,39 +481,39 @@
|
|||
[#1220](https://github.com/the-djmaze/snappymail/issues/1220)
|
||||
- "Request failed" on message move
|
||||
[#1220](https://github.com/the-djmaze/snappymail/issues/1220)
|
||||
- Unwrapped text nodes attached to squire._root
|
||||
[#1234](https://github.com/the-djmaze/snappymail/pull/1234) by @SergeyMosin
|
||||
- Unwrapped text nodes attached to squire._root by @SergeyMosin
|
||||
[#1234](https://github.com/the-djmaze/snappymail/pull/1234)
|
||||
- Extra wrapper div is added in Squire every time a Draft is open (or closed) after save.
|
||||
[#1208](https://github.com/the-djmaze/snappymail/issues/1208)
|
||||
- foreach() argument must be of type array|object
|
||||
[#1237](https://github.com/the-djmaze/snappymail/issues/1237)
|
||||
- `<font>` tag 'style' is lost in replies
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- unseen indicator is not shown in thread view when 'listGrouped' settings is false
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- `<font>` tag 'style' is lost in replies by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- unseen indicator is not shown in thread view when 'listGrouped' settings is false by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- TOTP plugin is dependent on ctype
|
||||
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)
|
||||
|
||||
### Nextcloud
|
||||
- iFrame mode: click on unified search result opens inner iFrame
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin
|
||||
- Improve UX of "Put in Calendar" option in plugin
|
||||
[#1259](https://github.com/the-djmaze/snappymail/pull/1259) by @theronakpatel
|
||||
- iFrame mode: click on unified search result opens inner iFrame by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened. by @SergeyMosin
|
||||
[#1248](https://github.com/the-djmaze/snappymail/pull/1248)
|
||||
- Improve UX of "Put in Calendar" option in plugin by @theronakpatel
|
||||
[#1259](https://github.com/the-djmaze/snappymail/pull/1259)
|
||||
|
||||
|
||||
## 2.28.4 – 2023-07-10
|
||||
|
||||
### Added
|
||||
- application.ini msg_default_action
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
|
||||
- application.ini view_show_next_message
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
|
||||
- application.ini view_images
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
|
||||
- nextcloud add ability to include custom php file in InstallStep migration
|
||||
[#1197](https://github.com/the-djmaze/snappymail/pull/1197) by @SergeyMosin
|
||||
- application.ini msg_default_action by @SergeyMosin
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
|
||||
- application.ini view_show_next_message by @SergeyMosin
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
|
||||
- application.ini view_images by @SergeyMosin
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
|
||||
- nextcloud add ability to include custom php file in InstallStep migration by @SergeyMosin
|
||||
[#1197](https://github.com/the-djmaze/snappymail/pull/1197)
|
||||
- Support plugin for Squire editor
|
||||
[#1192](https://github.com/the-djmaze/snappymail/issues/1192)
|
||||
|
||||
|
|
@ -905,14 +521,14 @@
|
|||
- only show 'Add "domain.tld" as an application for mailto links?' message after login (firefox shows the message on every reload otherwise).
|
||||
[#1204](https://github.com/the-djmaze/snappymail/issues/1204)
|
||||
- Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance
|
||||
- New bidi buttons to Squire editor
|
||||
[#1200](https://github.com/the-djmaze/snappymail/pull/1200) by @rezaei92
|
||||
- New bidi buttons to Squire editor by @rezaei92
|
||||
[#1200](https://github.com/the-djmaze/snappymail/pull/1200)
|
||||
|
||||
### Fixed
|
||||
- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT
|
||||
[#1205](https://github.com/the-djmaze/snappymail/issues/1205)
|
||||
- 'reloadTime' function result is passed into 'setInterval' instead of the function
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin
|
||||
- 'reloadTime' function result is passed into 'setInterval' instead of the function by @SergeyMosin
|
||||
[#1204](https://github.com/the-djmaze/snappymail/pull/1204)
|
||||
- UNKNOWN-CTE Invalid data in MIME part
|
||||
[#1186](https://github.com/the-djmaze/snappymail/issues/1186)
|
||||
|
||||
|
|
@ -975,10 +591,10 @@
|
|||
### Changed
|
||||
- Optical issue with input fields for mail and folder search
|
||||
[#1149](https://github.com/the-djmaze/snappymail/issues/1149)
|
||||
- Update Chinese translation
|
||||
[#1157](https://github.com/the-djmaze/snappymail/pull/1157) by @mayswind
|
||||
- Update Polish translation
|
||||
[#1156](https://github.com/the-djmaze/snappymail/pull/1156) by @tinola
|
||||
- Update Chinese translation by @mayswind
|
||||
[#1157](https://github.com/the-djmaze/snappymail/pull/1157)
|
||||
- Update Polish translation by @tinola
|
||||
[#1156](https://github.com/the-djmaze/snappymail/pull/1156)
|
||||
|
||||
### Fixed
|
||||
- Undefined SIG constants
|
||||
|
|
@ -1007,18 +623,18 @@
|
|||
- Allow empty message body when there are attachments
|
||||
[#1052](https://github.com/the-djmaze/snappymail/issues/1052)
|
||||
- PHP inherit logger as Trait
|
||||
- Update Portuguese
|
||||
[#1124](https://github.com/the-djmaze/snappymail/pull/1124) by @ner00
|
||||
- Update Traditional Chinese (Taiwan)
|
||||
[#1107](https://github.com/the-djmaze/snappymail/pull/1107) by @chiyi4488
|
||||
- Update Russian
|
||||
[#1108](https://github.com/the-djmaze/snappymail/pull/1108) by @konkere
|
||||
- Update Italian
|
||||
[#1094](https://github.com/the-djmaze/snappymail/pull/1094) by @cm-schl
|
||||
- Update French
|
||||
[#1102](https://github.com/the-djmaze/snappymail/pull/1102) by @hguilbert
|
||||
- Update German
|
||||
[#1087](https://github.com/the-djmaze/snappymail/pull/1087) by @cm-schl
|
||||
- Update Portuguese by @ner00
|
||||
[#1124](https://github.com/the-djmaze/snappymail/pull/1124)
|
||||
- Update Traditional Chinese (Taiwan) by @chiyi4488
|
||||
[#1107](https://github.com/the-djmaze/snappymail/pull/1107)
|
||||
- Update Russian by @konkere
|
||||
[#1108](https://github.com/the-djmaze/snappymail/pull/1108)
|
||||
- Update Italian by @cm-schl
|
||||
[#1094](https://github.com/the-djmaze/snappymail/pull/1094)
|
||||
- Update French by @hguilbert
|
||||
[#1102](https://github.com/the-djmaze/snappymail/pull/1102)
|
||||
- Update German by @cm-schl
|
||||
[#1087](https://github.com/the-djmaze/snappymail/pull/1087)
|
||||
|
||||
### Fixed
|
||||
- Show messagelist timeout/abort error for
|
||||
|
|
@ -1079,8 +695,8 @@
|
|||
- Workaround Upgrade with Nextcloud stuck in Maintenance mode
|
||||
[#1046](https://github.com/the-djmaze/snappymail/issues/1046)
|
||||
- Use Actions()->decodeRawKey()
|
||||
- French language updated
|
||||
[#1045](https://github.com/the-djmaze/snappymail/pull/1045) by @hguilbert
|
||||
- French language updated by @hguilbert
|
||||
[#1045](https://github.com/the-djmaze/snappymail/pull/1045)
|
||||
- Sorting in folders not working
|
||||
[#1022](https://github.com/the-djmaze/snappymail/issues/1022)
|
||||
- Allow saving draft with empty body
|
||||
|
|
@ -1144,15 +760,15 @@
|
|||
### Changed
|
||||
- Improved FolderCollection handling
|
||||
- MODSEQ requires 64-bit int
|
||||
- Update russian language
|
||||
[#994](https://github.com/the-djmaze/snappymail/pull/994) by @Akrobs
|
||||
- Update russian language by @Akrobs
|
||||
[#994](https://github.com/the-djmaze/snappymail/pull/994)
|
||||
- Don't make font bigger when screen > 1400px
|
||||
- Put top menu "accountPlace" inside top-system-dropdown-id
|
||||
- Put attachment controls inside attachmentsPlace
|
||||
- Show message toolbar on screens > 1400px
|
||||
[#970](https://github.com/the-djmaze/snappymail/issues/970)
|
||||
- Chinese updated
|
||||
[#1011](https://github.com/the-djmaze/snappymail/pull/1011) by @mayswind
|
||||
- Chinese updated by @mayswind
|
||||
[#1011](https://github.com/the-djmaze/snappymail/pull/1011)
|
||||
- Prevent folder/messages flags conflict by using the right name `attributes` for Folders
|
||||
- FolderInformation() use jsonSerialize()
|
||||
- Squire space handling on paste use `\u00A0` instead of ` ` for
|
||||
|
|
@ -1195,16 +811,16 @@
|
|||
|
||||
### Changed
|
||||
- Reduce/simplify CSS footprint
|
||||
- Use the System/Browser font by default
|
||||
[#988](https://github.com/the-djmaze/snappymail/pull/988) by @HeySora
|
||||
- Use the System/Browser font by default by @HeySora
|
||||
[#988](https://github.com/the-djmaze/snappymail/pull/988)
|
||||
- Make layout fully responsive using matchMedia('(max-width: 799px)')
|
||||
- Move brotli and gzip compress option to application.ini
|
||||
- After page refreshes in background whilst editing a draft, the space bar stops working
|
||||
[#860](https://github.com/the-djmaze/snappymail/issues/860)
|
||||
- Updated Portuguese
|
||||
[#984](https://github.com/the-djmaze/snappymail/pull/984) by @ner00
|
||||
- Updated French
|
||||
[#985](https://github.com/the-djmaze/snappymail/pull/985) by @hguilbert
|
||||
- Updated Portuguese by @ner00
|
||||
[#984](https://github.com/the-djmaze/snappymail/pull/984)
|
||||
- Updated French by @hguilbert
|
||||
[#985](https://github.com/the-djmaze/snappymail/pull/985)
|
||||
|
||||
### Fixed
|
||||
- unset border-box for message body
|
||||
|
|
@ -1300,8 +916,8 @@
|
|||
- Workaround "server connection error"
|
||||
[#936](https://github.com/the-djmaze/snappymail/issues/936)
|
||||
- Cleanup AbstractViewPopup handling
|
||||
- Italian translation
|
||||
[#948](https://github.com/the-djmaze/snappymail/pull/948) by @lota
|
||||
- Italian translation by @lota
|
||||
[#948](https://github.com/the-djmaze/snappymail/pull/948)
|
||||
- Replace Element.fromHTML() with createElement()
|
||||
|
||||
### Fixed
|
||||
|
|
@ -1349,8 +965,8 @@
|
|||
## 2.25.5 – 2023-02-09
|
||||
|
||||
### Added
|
||||
- New dark themes
|
||||
[#925](https://github.com/the-djmaze/snappymail/pull/925) by @TheCuteFoxxy
|
||||
- New dark themes by @TheCuteFoxxy
|
||||
[#925](https://github.com/the-djmaze/snappymail/pull/925)
|
||||
- External images option 'Always when DKIM is valid' for
|
||||
[#201](https://github.com/the-djmaze/snappymail/issues/201)
|
||||
- Image whitelist menu on message for
|
||||
|
|
@ -1390,8 +1006,8 @@
|
|||
### Added
|
||||
- message maximum quoted text level for speed.
|
||||
[#902](https://github.com/the-djmaze/snappymail/issues/902)
|
||||
- LoveDark Theme
|
||||
[#913](https://github.com/the-djmaze/snappymail/pull/913) by @TheCuteFoxxy
|
||||
- LoveDark Theme by @TheCuteFoxxy
|
||||
[#913](https://github.com/the-djmaze/snappymail/pull/913)
|
||||
|
||||
### Changed
|
||||
- Moved pluginEnable() to \SnappyMail\Repository::enablePackage()
|
||||
|
|
@ -1473,8 +1089,8 @@
|
|||
[#188](https://github.com/the-djmaze/snappymail/issues/188)
|
||||
- /static/.htaccess as mentioned by @dbiczo in
|
||||
[#895](https://github.com/the-djmaze/snappymail/issues/895)
|
||||
- Update nb-NO language
|
||||
[#896](https://github.com/the-djmaze/snappymail/pull/896) by @master3395
|
||||
- Update nb-NO language by @master3395
|
||||
[#896](https://github.com/the-djmaze/snappymail/pull/896)
|
||||
- \RainLoop\Utils::SetCookie() to \SnappyMail\Cookies::set()
|
||||
- Merge \MailSo\Base\Utils::RecRmDir() and \MailSo\Base\Utils::RecTimeDirRemove()
|
||||
|
||||
|
|
@ -1499,8 +1115,8 @@
|
|||
- Support RFC 8689
|
||||
- Nextcloud move themes outside of app folder
|
||||
[#875](https://github.com/the-djmaze/snappymail/issues/875)
|
||||
- Add check to unregister system addressbook
|
||||
[#879](https://github.com/the-djmaze/snappymail/pull/879) by @akhil1508
|
||||
- Add check to unregister system addressbook by @akhil1508
|
||||
[#879](https://github.com/the-djmaze/snappymail/pull/879)
|
||||
|
||||
### Changed
|
||||
- material-design checkbox use hidden input for accessibility
|
||||
|
|
@ -1524,8 +1140,8 @@
|
|||
## 2.25.0 – 2023-01-26
|
||||
|
||||
### Changed
|
||||
- Right margin to folder search input wrapper
|
||||
[#871](https://github.com/the-djmaze/snappymail/pull/871) by @codiflow
|
||||
- Right margin to folder search input wrapper by @codiflow
|
||||
[#871](https://github.com/the-djmaze/snappymail/pull/871)
|
||||
- Almost all request params now use JavaScript camelCase instead of CamelCase
|
||||
- #rl-content to use flex for better layout control
|
||||
- Decode some Copernica link tracking
|
||||
|
|
@ -1554,8 +1170,8 @@
|
|||
### Added
|
||||
- Preparations for RFC 8689
|
||||
- Add admin setting for the contacts suggestions limit
|
||||
- A button to clear folder search input field
|
||||
[#847](https://github.com/the-djmaze/snappymail/pull/847) by @codiflow
|
||||
- A button to clear folder search input field by @codiflow
|
||||
[#847](https://github.com/the-djmaze/snappymail/pull/847)
|
||||
- Preparations for custom SMTP credentials handling for
|
||||
[#859](https://github.com/the-djmaze/snappymail/issues/859)
|
||||
[#458](https://github.com/the-djmaze/snappymail/issues/458)
|
||||
|
|
@ -1571,8 +1187,8 @@
|
|||
[#849](https://github.com/the-djmaze/snappymail/issues/849)
|
||||
- 64-bit PHP was required
|
||||
[#852](https://github.com/the-djmaze/snappymail/issues/852)
|
||||
- Update fr-FR translations
|
||||
[#854](https://github.com/the-djmaze/snappymail/pull/854) by @dominiquefournier
|
||||
- Update fr-FR translations by @dominiquefournier
|
||||
[#854](https://github.com/the-djmaze/snappymail/pull/854)
|
||||
- Move createDomain & createDomainAlias buttons below table
|
||||
- Move some Nextcloud SnappyMailHelper code to new InstallStep
|
||||
- Cleanup messages cache handling
|
||||
|
|
@ -1598,8 +1214,8 @@
|
|||
### Added
|
||||
- Support for search criterias ON, SENTON, SENTSINCE and SENTBEFORE
|
||||
- New 'forward as attachment' glyph
|
||||
- NC integration app metadata: add links to admin and dev docs
|
||||
[#820](https://github.com/the-djmaze/snappymail/pull/820) by @p-bo
|
||||
- NC integration app metadata: add links to admin and dev docs by @p-bo
|
||||
[#820](https://github.com/the-djmaze/snappymail/pull/820)
|
||||
- Clicking on messagelist grouped date/from searches on that date/from
|
||||
[#815](https://github.com/the-djmaze/snappymail/issues/815)
|
||||
- Nextcloud Improved file picker layout
|
||||
|
|
@ -1613,16 +1229,16 @@
|
|||
- Resolve layout issues by changing from `fixed` to `relative`
|
||||
[#686](https://github.com/the-djmaze/snappymail/issues/686)
|
||||
- Cache handling of messagelist changed due to etag issues
|
||||
- Improve german and italian translation
|
||||
[#846](https://github.com/the-djmaze/snappymail/pull/846) by @cm-schl
|
||||
- Improve german and italian translation by @cm-schl
|
||||
[#846](https://github.com/the-djmaze/snappymail/pull/846)
|
||||
|
||||
### Fixed
|
||||
- Searching on Nextcloud search
|
||||
[#787](https://github.com/the-djmaze/snappymail/issues/787)
|
||||
- Workaround another Nextcloud disallowed .htaccess
|
||||
[#790](https://github.com/the-djmaze/snappymail/issues/790)
|
||||
- Compact display of folders in nextcloud
|
||||
[#824](https://github.com/the-djmaze/snappymail/pull/824) by @makoehr
|
||||
- Compact display of folders in nextcloud by @makoehr
|
||||
[#824](https://github.com/the-djmaze/snappymail/pull/824)
|
||||
- Admin -> Contacts PDO test failed when using different AddressBookInterface
|
||||
- KolabAddressBook errors
|
||||
- Forgot debug js/css setting change in Nextcloud
|
||||
|
|
@ -1674,8 +1290,8 @@
|
|||
### Changed
|
||||
- Disable sorting when viewing message thread
|
||||
[#445](https://github.com/the-djmaze/snappymail/issues/445)
|
||||
- Update Chinese translation
|
||||
[#794](https://github.com/the-djmaze/snappymail/pull/794) by @mayswind
|
||||
- Update Chinese translation by @mayswind
|
||||
[#794](https://github.com/the-djmaze/snappymail/pull/794)
|
||||
- No need to call IMAP EXAMINE when current folder already SELECT
|
||||
- Thread view now has tree indentation
|
||||
|
||||
|
|
@ -1788,8 +1404,8 @@
|
|||
### Fixed
|
||||
- Loosing HTML signature in account identity under settings
|
||||
[#750](https://github.com/the-djmaze/snappymail/issues/750)
|
||||
- Plugin configuration did not load anymore when type was SELECTION
|
||||
[#753](https://github.com/the-djmaze/snappymail/pull/753) by @cm-schl
|
||||
- Plugin configuration did not load anymore when type was SELECTION by @cm-schl
|
||||
[#753](https://github.com/the-djmaze/snappymail/pull/753)
|
||||
- Nextcloud Default theme shows gray text on gray background
|
||||
[#754](https://github.com/the-djmaze/snappymail/issues/754)
|
||||
- Only run JSON hooks when $sAction is set
|
||||
|
|
@ -2053,8 +1669,8 @@
|
|||
[#638](https://github.com/the-djmaze/snappymail/issues/638)
|
||||
|
||||
### Fixed
|
||||
- ldap-identities-plugin
|
||||
[#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
|
||||
- ldap-identities-plugin by @cm-schl
|
||||
[#647](https://github.com/the-djmaze/snappymail/pull/647)
|
||||
- OpenSSL v3 ciphers issue
|
||||
[#641](https://github.com/the-djmaze/snappymail/issues/641)
|
||||
|
||||
|
|
@ -2153,8 +1769,8 @@
|
|||
[#620](https://github.com/the-djmaze/snappymail/issues/620)
|
||||
- PutinICS does is not working for all calendar events
|
||||
[#622](https://github.com/the-djmaze/snappymail/issues/622)
|
||||
- Update readme
|
||||
[#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
|
||||
- Update readme by @cm-schl
|
||||
[#617](https://github.com/the-djmaze/snappymail/pull/617)
|
||||
|
||||
|
||||
## 2.20.0 – 2022-11-03
|
||||
|
|
@ -2248,8 +1864,8 @@
|
|||
[#638](https://github.com/the-djmaze/snappymail/issues/638)
|
||||
|
||||
### Fixed
|
||||
- ldap-identities-plugin
|
||||
[#647](https://github.com/the-djmaze/snappymail/pull/647) by @cm-schl
|
||||
- ldap-identities-plugin by @cm-schl
|
||||
[#647](https://github.com/the-djmaze/snappymail/pull/647)
|
||||
- OpenSSL v3 ciphers issue
|
||||
[#641](https://github.com/the-djmaze/snappymail/issues/641)
|
||||
|
||||
|
|
@ -2348,8 +1964,8 @@
|
|||
[#620](https://github.com/the-djmaze/snappymail/issues/620)
|
||||
- PutinICS does is not working for all calendar events
|
||||
[#622](https://github.com/the-djmaze/snappymail/issues/622)
|
||||
- Update readme
|
||||
[#617](https://github.com/the-djmaze/snappymail/pull/617) by @cm-schl
|
||||
- Update readme by @cm-schl
|
||||
[#617](https://github.com/the-djmaze/snappymail/pull/617)
|
||||
|
||||
|
||||
## 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
|
||||
* Auth failed attempts written to syslog
|
||||
* Added Fail2ban instructions
|
||||
* ES2020
|
||||
* ES2018
|
||||
* PHP 7.4+ required
|
||||
* PHP mbstring extension required
|
||||
* PHP replaced pclZip with PharData and ZipArchive
|
||||
|
|
@ -66,7 +66,7 @@ This fork of RainLoop has the following changes:
|
|||
* CRLF => LF line endings
|
||||
* Embed boot.js and boot.css into index.html
|
||||
* Removal of old JavaScript code (things are native these days)
|
||||
* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
|
||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||
* Split Admin specific JavaScript code from User code
|
||||
* Split Sieve specific JavaScript code from User code
|
||||
|
|
@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 84.925 |
|
||||
|app.js |4.207.787 | 447.263 |
|
||||
|boot.js | 868.735 | 4.343 |
|
||||
|libs.js | 658.812 | 233.728 |
|
||||
|sieve.js | 0 | 91.418 |
|
||||
|admin.js |2.170.153 | 82.845 |
|
||||
|app.js |4.207.787 | 430.201 |
|
||||
|boot.js | 868.735 | 4.147 |
|
||||
|libs.js | 658.812 | 201.911 |
|
||||
|sieve.js | 0 | 84.703 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.240.095 | 861.962 |
|
||||
|TOTAL |8.240.095 | 804.092 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|
||||
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|
||||
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|
||||
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|
||||
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|
||||
|admin.min.js | 256.831 | 40.733 | 73.606 | 13.649 | 60.877 | 12.228 |
|
||||
|app.min.js | 515.367 | 195.260 |139.456 | 66.275 |110.485 | 56.682 |
|
||||
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
|
||||
|libs.min.js | 584.772 | 92.482 |180.901 | 34.729 |155.182 | 31.017 |
|
||||
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|
||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|
||||
|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
|
||||
|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
|
||||
|TOTAL user |1.217.635 | 289.829 |358.761 |102.208 |299.485 | 88.701 |
|
||||
|TOTAL user+sieve |1.217.635 | 330.993 |358.761 |112.573 |299.485 | 98.060 |
|
||||
|TOTAL admin | 959.099 | 135.302 |292.911 | 49.582 |249.877 | 44.247 |
|
||||
|
||||
For a user it is around 66% smaller and faster than traditional RainLoop.
|
||||
For a user it is around 69% smaller and faster than traditional RainLoop.
|
||||
|
||||
### CSS changes
|
||||
|
||||
|
|
@ -188,12 +188,12 @@ For a user it is around 66% smaller and faster than traditional RainLoop.
|
|||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|
||||
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|
||||
|app.css | 340.331 | 84.531 | 46.946 | 17.627 | 15.111 |
|
||||
|app.min.css | 274.947 | 67.906 | 39.647 | 15.544 | 13.567 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.880 | | 7.045 | 6.127 |
|
||||
|admin.min.css | | 24.959 | | 6.368 | 5.615 |
|
||||
|admin.css | | 30.573 | | 7.011 | 6.101 |
|
||||
|admin.min.css | | 24.689 | | 6.334 | 5.575 |
|
||||
|
||||
### PGP
|
||||
RainLoop uses the old OpenPGP.js v2
|
||||
|
|
@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
|||
|
||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|
||||
|openpgp.min.js | 330.742 | 546.309 |102.388 | 169.249 | 84.241 | 138.751 |
|
||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ Currently due to the fast development only the latest version receives security
|
|||
|
||||
| Version | Supported |
|
||||
| -------- | --------- |
|
||||
| 2.34.x | ✔ |
|
||||
| < 2.34.0 | ❌ |
|
||||
| 2.30.x | ✔ |
|
||||
| < 2.30.0 | ❌ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
@ -36,5 +36,5 @@ YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
|
|||
## Publishing and Credits
|
||||
|
||||
I will analyze and fix the reported issue as fast as possible.
|
||||
Together with the reporter, I plan the disclosure of the found and fixed vulnerability.
|
||||
Together with the reporter I plan the disclosure of the found and fixed vulnerability.
|
||||
Credits to the reporter are granted and can be included in all public communication if desired.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ if (isset($options['plugins'])) {
|
|||
require(ROOT_DIR . '/build/plugins.php');
|
||||
}
|
||||
|
||||
$gulp = trim(shell_exec('which gulp'));
|
||||
$gulp = trim(`which gulp`);
|
||||
if (!$gulp) {
|
||||
exit('gulp not installed, run as root: npm install --global gulp-cli');
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ $options['aur'] = isset($options['aur']);
|
|||
|
||||
// Docker build
|
||||
$options['docker'] = isset($options['docker']);
|
||||
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
|
||||
if ($options['docker'] && !($docker = trim(`which docker`))) {
|
||||
exit('docker not found');
|
||||
}
|
||||
if ($options['docker'] && $options['aur']) {
|
||||
|
|
@ -60,7 +60,7 @@ if (!isset($options['skip-gulp'])) {
|
|||
|
||||
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
|
||||
|
||||
if ($gzip = trim(shell_exec('which gzip'))) {
|
||||
if ($gzip = trim(`which gzip`)) {
|
||||
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
|
||||
passthru("{$gzip} -k --best {$cmddir}/js/*.js");
|
||||
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
|
||||
|
|
@ -70,7 +70,7 @@ if (!isset($options['skip-gulp'])) {
|
|||
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
|
||||
}
|
||||
|
||||
if ($brotli = trim(shell_exec('which brotli'))) {
|
||||
if ($brotli = trim(`which brotli`)) {
|
||||
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
|
||||
passthru("{$brotli} -k --best {$cmddir}/js/*.js");
|
||||
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
|
||||
|
|
@ -188,7 +188,7 @@ file_put_contents("{$destPath}core.json", '{
|
|||
// Arch User Repository
|
||||
if ($options['aur']) {
|
||||
// extension_loaded('blake2')
|
||||
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
|
||||
if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) {
|
||||
function b2sum($file) {
|
||||
$file = escapeshellarg($file);
|
||||
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'External/User/ko';
|
||||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
import { pInt } from 'Common/Utils';
|
||||
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||
|
||||
import {
|
||||
|
|
@ -27,7 +27,7 @@ import {
|
|||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
|
||||
import { i18n, reloadTime, getErrorMessage } from 'Common/Translator';
|
||||
import { i18n, reloadTime } from 'Common/Translator';
|
||||
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||
|
|
@ -88,8 +88,6 @@ export class AppUser extends AbstractApp {
|
|||
this.messageList = MessagelistUserStore;
|
||||
|
||||
this.ask = AskPopupView;
|
||||
|
||||
this.loadAccountsAndIdentities = loadAccountsAndIdentities;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -114,7 +112,7 @@ export class AppUser extends AbstractApp {
|
|||
case FolderType.Trash:
|
||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
||||
nSetSystemFoldersNotification = iFolderType;
|
||||
bDelete = bDelete/* || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()*/
|
||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
|
||||
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
||||
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
||||
break;
|
||||
|
|
@ -149,10 +147,8 @@ export class AppUser extends AbstractApp {
|
|||
}
|
||||
|
||||
logout() {
|
||||
Remote.request('Logout', (iError, data) =>
|
||||
iError ? alert('Logout error: ' + getErrorMessage(iError, data))
|
||||
: rl.logoutReload(Settings.app('customLogoutLink'))
|
||||
);
|
||||
localStorage.removeItem('register_protocol_offered');
|
||||
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
|
||||
}
|
||||
|
||||
bootstart() {
|
||||
|
|
@ -184,15 +180,15 @@ export class AppUser extends AbstractApp {
|
|||
SettingsUserStore.init();
|
||||
ContactUserStore.init();
|
||||
|
||||
loadFolders((success, error) => {
|
||||
loadFolders(value => {
|
||||
try {
|
||||
if (success) {
|
||||
if (value) {
|
||||
startScreens([
|
||||
MailBoxUserScreen,
|
||||
SettingsUserScreen
|
||||
]);
|
||||
|
||||
setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
|
||||
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
|
||||
|
||||
loadAccountsAndIdentities();
|
||||
|
||||
|
|
@ -226,9 +222,19 @@ export class AppUser extends AbstractApp {
|
|||
SMimeUserStore.loadCertificates();
|
||||
|
||||
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||
|
||||
if (!localStorage.getItem('register_protocol_offered')) {
|
||||
// When auto-login is active
|
||||
navigator.registerProtocolHandler?.(
|
||||
'mailto',
|
||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
||||
(SettingsGet('title') || 'SnappyMail')
|
||||
);
|
||||
localStorage.setItem('register_protocol_offered', '1');
|
||||
}
|
||||
|
||||
} else {
|
||||
this.logout();
|
||||
alert('Folders error: ' + getErrorMessage(0, error))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -241,31 +247,18 @@ export class AppUser extends AbstractApp {
|
|||
|
||||
showMessageComposer(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);
|
||||
}
|
||||
showScreenPopup(ComposePopupView, params);
|
||||
}
|
||||
}
|
||||
|
||||
AskPopupView.password = function(sAskDesc, btnText, ask) {
|
||||
AskPopupView.password = function(sAskDesc, btnText) {
|
||||
return new Promise(resolve => {
|
||||
this.showModal([
|
||||
sAskDesc,
|
||||
view => resolve({
|
||||
password:view.passphrase(),
|
||||
username:/*ask & 2 ? */view.username(),
|
||||
remember:/*ask & 4 ? */view.remember(),
|
||||
rememberPermanent: view.rememberPermanent(),
|
||||
}),
|
||||
view => resolve({password:view.passphrase(), remember:view.remember()}),
|
||||
() => resolve(null),
|
||||
true,
|
||||
ask || 1,
|
||||
5,
|
||||
btnText
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -71,9 +71,7 @@ MessageSetAction = {
|
|||
SetSeen: 0,
|
||||
UnsetSeen: 1,
|
||||
SetFlag: 2,
|
||||
UnsetFlag: 3,
|
||||
SetDeleted: 4,
|
||||
UnsetDeleted: 5
|
||||
UnsetFlag: 3
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint key-spacing: 0 */
|
||||
/* eslint quote-props: 0 */
|
||||
|
||||
import { arrayLength, pInt } from 'Common/Utils';
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
|
||||
export const RFC822 = 'message/rfc822';
|
||||
|
||||
|
|
@ -274,7 +274,8 @@ export const FileInfo = {
|
|||
},
|
||||
|
||||
friendlySize: bytes => {
|
||||
bytes = pInt(bytes);
|
||||
bytes = parseInt(bytes, 10);
|
||||
bytes = isFinite(bytes) ? bytes : 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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { RFC822 } from 'Common/File';
|
||||
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
||||
import { baseCollator } from 'Common/Translator';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { isArray, arrayLength, pInt } from 'Common/Utils';
|
||||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
|
|
@ -10,13 +8,13 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
let refreshInterval,
|
||||
// Default every 15 minutes
|
||||
refreshFoldersInterval = 900000;
|
||||
// Default every 5 minutes
|
||||
refreshFoldersInterval = 300000;
|
||||
|
||||
export const
|
||||
|
||||
setRefreshFoldersInterval = minutes => {
|
||||
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
|
||||
refreshFoldersInterval = Math.max(5, minutes) * 60000;
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = setInterval(() => {
|
||||
const cF = FolderUserStore.currentFolderFullName(),
|
||||
|
|
@ -29,7 +27,7 @@ setRefreshFoldersInterval = minutes => {
|
|||
|
||||
sortFolders = folders => {
|
||||
try {
|
||||
let collator = baseCollator(true);
|
||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
folders.sort((a, b) =>
|
||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||
);
|
||||
|
|
@ -199,7 +197,7 @@ dropFilesInFolder = (sFolderFullName, files) => {
|
|||
data.append('folder', sFolderFullName);
|
||||
data.append('appendFile', file);
|
||||
Remote.request('FolderAppend', (iError, data)=>{
|
||||
iError && console.error(data.message);
|
||||
iError && console.error(data.ErrorMessage);
|
||||
0 == --count
|
||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||
&& MessagelistUserStore.reload(true, true);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
|||
|
||||
const
|
||||
tmpl = createElement('template'),
|
||||
|
||||
turndown = new TurndownService(),
|
||||
|
||||
htmlre = /[&<>"']/g,
|
||||
httpre = /^(https?:)?\/\//i,
|
||||
htmlmap = {
|
||||
|
|
@ -17,34 +14,14 @@ const
|
|||
"'": '''
|
||||
},
|
||||
|
||||
keepTagContent = 'form,button,data', // font
|
||||
|
||||
allowedTags = [
|
||||
// Structural Elements:
|
||||
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
|
||||
'article','aside','header','footer','main','section',
|
||||
'details','summary','nav',
|
||||
// List Elements
|
||||
'dd','dl','dt','li','ol','ul',
|
||||
// Text Formatting Elements
|
||||
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
|
||||
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
|
||||
'span','strong','sub','sup','time','u','var',
|
||||
// Deprecated by HTML Standard
|
||||
'acronym','big','center','dir','font','marquee',
|
||||
'nobr','plaintext','rb','rtc','strike','tt',
|
||||
// Media Elements
|
||||
'img',//'picture','source',
|
||||
// Table Elements
|
||||
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
|
||||
// Disallowed but converted later
|
||||
'style','xmp'
|
||||
disallowedTags = [
|
||||
'svg','script','title','link','base','meta',
|
||||
'input','output','select','button','textarea',
|
||||
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
|
||||
// not supported by <template> element
|
||||
// ,'html','head','body'
|
||||
].join(','),
|
||||
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG'
|
||||
],
|
||||
|
||||
blockquoteSwitcher = () => {
|
||||
SettingsUserStore.collapseBlockquotes() &&
|
||||
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
||||
|
|
@ -122,10 +99,8 @@ const
|
|||
},
|
||||
|
||||
cleanCSS = source =>
|
||||
source.trim()
|
||||
.replace(/;\s*-[^;]+/g, '')
|
||||
.replace(/^\s*-[^;]+(;|$)/g, '')
|
||||
.replace(/white-space[^;]+/g, '')
|
||||
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
|
||||
.replace(/white-space[^;]+(;|$)/g, '')
|
||||
// Drop Microsoft Office style properties
|
||||
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
||||
,
|
||||
|
|
@ -167,15 +142,14 @@ const
|
|||
if (source) {
|
||||
source = source
|
||||
// strip comments
|
||||
.replace(/\/\*[\s\S]*?\*\//gi, '')
|
||||
// strip MS Word comments
|
||||
.replace(/<!--[\s\S]*?-->/gi, '')
|
||||
// strip HTML, as < is no CSS combinator anyway
|
||||
.replace(/<[\s\S]*/gi, '');
|
||||
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
|
||||
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
|
||||
// strip import statements
|
||||
.replace(/@import .*?;/gi , '')
|
||||
// strip keyframe statements
|
||||
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
|
||||
|
||||
// unified regex to match css & media queries together
|
||||
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
|
||||
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
|
||||
arr;
|
||||
|
||||
while (true) {
|
||||
|
|
@ -184,7 +158,7 @@ const
|
|||
break;
|
||||
}
|
||||
|
||||
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
|
||||
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
|
||||
// Never have more than a single line break in a row
|
||||
.replace(/\n+/, "\n")
|
||||
// Remove :root and html
|
||||
|
|
@ -196,14 +170,13 @@ const
|
|||
css.push({
|
||||
selector: selector,
|
||||
type: 'media',
|
||||
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
|
||||
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
|
||||
});
|
||||
} else if (selector && !selector.includes('@')) {
|
||||
// we have standard css
|
||||
// ignores @import, @keyframe, @font-face statements
|
||||
css.push({
|
||||
selector: selector,
|
||||
rules: cleanCSS(arr[4])
|
||||
rules: cleanCSS(arr[6])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -232,7 +205,6 @@ export const
|
|||
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
||||
|
||||
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
||||
minFontSize = parseInt(SettingsUserStore.minFontSize()),
|
||||
|
||||
result = {
|
||||
hasExternals: false,
|
||||
|
|
@ -240,8 +212,6 @@ export const
|
|||
linkedData: []
|
||||
},
|
||||
|
||||
isMsg = !!msgId,
|
||||
|
||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||
findLocationByCid = cId => {
|
||||
const attachment = findAttachmentByCid(cId);
|
||||
|
|
@ -258,15 +228,6 @@ export const
|
|||
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
|
||||
},
|
||||
allowedAttributes = [
|
||||
// SnappyMail
|
||||
// 'data-x-href-broken',
|
||||
// 'data-x-href-tracking',
|
||||
'data-x-src',
|
||||
// 'data-x-src-hidden',
|
||||
// 'data-x-src-tracking',
|
||||
// 'data-x-src-broken',
|
||||
// 'data-x-style-url',
|
||||
// 'data-x-style-broken-urls',
|
||||
// defaults
|
||||
'name',
|
||||
'dir', 'lang', 'style', 'title',
|
||||
|
|
@ -294,8 +255,9 @@ export const
|
|||
'abbr', 'scope',
|
||||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
// others
|
||||
//'class', 'id', 'target'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG'
|
||||
];
|
||||
|
||||
if (SettingsUserStore.allowStyles()) {
|
||||
|
|
@ -309,8 +271,6 @@ export const
|
|||
// Not supported by <template> element
|
||||
// .replace(/<!doctype[^>]*>/gi, '')
|
||||
// .replace(/<\?xml[^>]*\?>/gi, '')
|
||||
// https://github.com/the-djmaze/snappymail/issues/1860
|
||||
.replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
|
||||
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
|
||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
||||
// .replace(/<\/?(html|head)[^>]*>/gi, '')
|
||||
|
|
@ -344,33 +304,13 @@ export const
|
|||
}
|
||||
});
|
||||
|
||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
|
||||
|
||||
tmpl.content.querySelectorAll(
|
||||
':not('+allowedTags+'),a:empty,span:empty'
|
||||
disallowedTags
|
||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||
).forEach(oElement => oElement.remove());
|
||||
/* // Is this slower or faster?
|
||||
).forEach(oElement => {
|
||||
if (!node || !node.contains(oElement)) {
|
||||
oElement.remove();
|
||||
node = oElement;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// https://github.com/the-djmaze/snappymail/issues/1641
|
||||
let body = tmpl.content.querySelector('.mail-body');
|
||||
[...tmpl.content.querySelectorAll('.mail-body + .mail-body')]
|
||||
.forEach(oElement => body.append(...oElement.childNodes));
|
||||
/*
|
||||
.forEach(oElement => {
|
||||
let bq = createElement('blockquote');
|
||||
bq.append(...oElement.childNodes);
|
||||
body.replaceWith(bq);
|
||||
});
|
||||
*/
|
||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
|
||||
|
||||
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
|
||||
const name = oElement.tagName,
|
||||
|
|
@ -391,13 +331,6 @@ export const
|
|||
return;
|
||||
}
|
||||
|
||||
if ('XMP' === name) {
|
||||
const pre = createElement('pre');
|
||||
pre.innerHTML = encodeHtml(oElement.innerHTML);
|
||||
oElement.replaceWith(pre);
|
||||
return;
|
||||
}
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||
if ('none' == oStyle.display
|
||||
|| 'hidden' == oStyle.visibility
|
||||
|
|
@ -484,7 +417,7 @@ export const
|
|||
}
|
||||
|
||||
// if (['CENTER','FORM'].includes(name)) {
|
||||
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
|
||||
if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
|
||||
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
||||
return;
|
||||
}
|
||||
|
|
@ -497,73 +430,70 @@ export const
|
|||
*/
|
||||
|
||||
let skipStyle = false;
|
||||
if (isMsg) {
|
||||
value = isMsg && delAttribute('src');
|
||||
if (value) {
|
||||
if ('IMG' === name) {
|
||||
oElement.loading = 'lazy';
|
||||
let attachment;
|
||||
if (value.startsWith('cid:'))
|
||||
{
|
||||
setAttribute('data-x-src', value);
|
||||
value = value.slice(4);
|
||||
attachment = findAttachmentByCid(value);
|
||||
if (attachment?.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
// oElement.title || (oElement.title = '+attachment.fileName);
|
||||
attachment.isInline(true);
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
value = delAttribute('src');
|
||||
if (value) {
|
||||
if ('IMG' === name) {
|
||||
oElement.loading = 'lazy';
|
||||
let attachment;
|
||||
if (value.startsWith('cid:'))
|
||||
{
|
||||
value = value.slice(4);
|
||||
setAttribute('data-x-src-cid', value);
|
||||
attachment = findAttachmentByCid(value);
|
||||
if (attachment?.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
oElement.title += ' ('+attachment.fileName+')';
|
||||
attachment.isInline(true);
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
else if ((attachment = findLocationByCid(value)))
|
||||
{
|
||||
if (attachment.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
}
|
||||
else if ((attachment = findLocationByCid(value)))
|
||||
{
|
||||
if (attachment.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
else if (detectHiddenImages
|
||||
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|
||||
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|
||||
|| (oStyle.width && 2 > pInt(oStyle.width))
|
||||
|| [
|
||||
'email.microsoftemail.com/open',
|
||||
'github.com/notifications/beacon/',
|
||||
'/track/open', // mandrillapp.com list-manage.com
|
||||
'google-analytics.com'
|
||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||
)) {
|
||||
skipStyle = true;
|
||||
oStyle.display = 'none';
|
||||
// setAttribute('style', 'display:none');
|
||||
setAttribute('data-x-src-hidden', value);
|
||||
// result.tracking = true;
|
||||
}
|
||||
else if (httpre.test(value))
|
||||
{
|
||||
let src = stripTracking(value);
|
||||
if (src != value) {
|
||||
result.tracking = true;
|
||||
setAttribute('data-x-src-tracking', value);
|
||||
}
|
||||
setAttribute('data-x-src', src);
|
||||
result.hasExternals = true;
|
||||
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
||||
}
|
||||
else if (value.startsWith('data:image/'))
|
||||
{
|
||||
oElement.src = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-src-broken', value);
|
||||
}
|
||||
else if (detectHiddenImages
|
||||
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|
||||
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|
||||
|| [
|
||||
'email.microsoftemail.com/open',
|
||||
'github.com/notifications/beacon/',
|
||||
'/track/open', // mandrillapp.com list-manage.com
|
||||
'google-analytics.com'
|
||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||
)) {
|
||||
skipStyle = true;
|
||||
oStyle.display = 'none';
|
||||
// setAttribute('style', 'display:none');
|
||||
setAttribute('data-x-src-hidden', value);
|
||||
// result.tracking = true;
|
||||
}
|
||||
else if (httpre.test(value))
|
||||
{
|
||||
let src = stripTracking(value);
|
||||
if (src != value) {
|
||||
result.tracking = true;
|
||||
setAttribute('data-x-src-tracking', value);
|
||||
}
|
||||
setAttribute('data-x-src', src);
|
||||
result.hasExternals = true;
|
||||
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
||||
}
|
||||
else if (value.startsWith('data:image/'))
|
||||
{
|
||||
oElement.src = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-src-broken', value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-src-broken', value);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAttribute('background')) {
|
||||
|
|
@ -626,13 +556,12 @@ export const
|
|||
if (urls_broken.length) {
|
||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
|
||||
}
|
||||
|
||||
/*
|
||||
// https://github.com/the-djmaze/snappymail/issues/1082
|
||||
if (minFontSize > pInt(oStyle.fontSize)) {
|
||||
oStyle.fontSize = minFontSize + "px";
|
||||
// oStyle.removeProperty('font-size');
|
||||
if (11 > pInt(oStyle.fontSize)) {
|
||||
oStyle.removeProperty('font-size');
|
||||
}
|
||||
|
||||
*/
|
||||
// Removes background and color
|
||||
// Many e-mails incorrectly only define one, not both
|
||||
// And in dark theme mode this kills the readability
|
||||
|
|
@ -650,7 +579,7 @@ export const
|
|||
}
|
||||
});
|
||||
|
||||
isMsg && blockquoteSwitcher();
|
||||
blockquoteSwitcher();
|
||||
|
||||
// return tmpl.content.firstChild;
|
||||
result.html = tmpl.innerHTML.trim();
|
||||
|
|
@ -662,9 +591,6 @@ export const
|
|||
* @returns {string}
|
||||
*/
|
||||
htmlToPlain = html => {
|
||||
if (SettingsUserStore.markdown()) {
|
||||
return htmlToMarkdown(html);
|
||||
}
|
||||
const
|
||||
hr = '⎯'.repeat(64),
|
||||
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
||||
|
|
@ -685,8 +611,6 @@ export const
|
|||
html = html
|
||||
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
||||
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
||||
// Remove line duplication
|
||||
.replace(/<br><\/div>/gi, '</div>')
|
||||
.replace(/\r?\n/g, '')
|
||||
.replace(/\s+/gm, ' ');
|
||||
|
||||
|
|
@ -701,8 +625,6 @@ export const
|
|||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||
|
||||
forEach('style', node => node.remove());
|
||||
|
||||
// lines
|
||||
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
||||
|
||||
|
|
@ -739,9 +661,7 @@ export const
|
|||
forEach('a', a => {
|
||||
let txt = a.textContent, href = a.href;
|
||||
return a.replaceWith(
|
||||
txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, ''))
|
||||
? txt
|
||||
: txt + ' ' + href + ' '
|
||||
txt.trim() == href || href.includes('mailto:') ? txt : txt + ' ' + href + ' '
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -763,11 +683,6 @@ export const
|
|||
return (tmpl.content.textContent || '').trim();
|
||||
},
|
||||
|
||||
htmlToMarkdown = html => {
|
||||
tmpl.innerHTML = html;
|
||||
return turndown.turndown(tmpl.content);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} plain
|
||||
* @param {boolean} findEmailAndLinksInText = false
|
||||
|
|
@ -838,9 +753,6 @@ export const
|
|||
};
|
||||
|
||||
rl.Utils = {
|
||||
cleanHtml: cleanHtml,
|
||||
htmlToPlain: htmlToPlain,
|
||||
plainToHtml: plainToHtml,
|
||||
htmlToMarkdown: htmlToMarkdown
|
||||
// markdownToHtml: md => marked.parse(md)
|
||||
plainToHtml: plainToHtml
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ export const
|
|||
|
||||
/**
|
||||
* @param {string} download
|
||||
* @param {string=} customSpecSuffix
|
||||
* @returns {string}
|
||||
*/
|
||||
attachmentDownload = (download) =>
|
||||
serverRequestRaw('Download', download),
|
||||
attachmentDownload = (download, customSpecSuffix) =>
|
||||
serverRequestRaw('Download', download, customSpecSuffix),
|
||||
|
||||
proxy = url =>
|
||||
BASE + '?/ProxyExternal/'
|
||||
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ export const
|
|||
}
|
||||
}
|
||||
if (Intl.RelativeTimeFormat) {
|
||||
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
|
||||
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
|
||||
return rtf.format(seconds, unit);
|
||||
}
|
||||
// Safari < 14
|
||||
abs = Math.abs(seconds);
|
||||
|
|
@ -61,7 +62,7 @@ export const
|
|||
* @returns {string}
|
||||
*/
|
||||
i18n = (key, valueList, defaulValue) => {
|
||||
let result = defaulValue ?? key;
|
||||
let result = null == defaulValue ? key : defaulValue;
|
||||
let path = key.split('/');
|
||||
if (I18N_DATA[path[0]] && path[1]) {
|
||||
result = I18N_DATA[path[0]][path[1]] || result;
|
||||
|
|
@ -184,9 +185,6 @@ export const
|
|||
|| '';
|
||||
},
|
||||
|
||||
getErrorMessage = (code, data) =>
|
||||
getNotification(code) || data?.messageAdditional || data?.message || data,
|
||||
|
||||
/**
|
||||
* @param {*} code
|
||||
* @returns {string}
|
||||
|
|
@ -220,6 +218,7 @@ export const
|
|||
}),
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} language
|
||||
* @param {boolean=} isEng = false
|
||||
* @returns {string}
|
||||
|
|
@ -229,8 +228,6 @@ export const
|
|||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||
null,
|
||||
language
|
||||
),
|
||||
|
||||
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
|
||||
);
|
||||
|
||||
init();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const
|
|||
|
||||
pInt = (value, defaultValue = 0) => {
|
||||
value = parseInt(value, 10);
|
||||
return isFinite(value) ? value : defaultValue;
|
||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||
},
|
||||
|
||||
defaultOptionsAfterRender = (domItem, item) =>
|
||||
|
|
|
|||
|
|
@ -18,21 +18,15 @@ import { AccountUserStore } from 'Stores/User/Account';
|
|||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||
|
||||
export const
|
||||
|
||||
// 1 = move, 2 = copy
|
||||
moveAction = ko.observable(0),
|
||||
moveAction = ko.observable(false),
|
||||
|
||||
dropdownsDetectVisibility = (() =>
|
||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||
).debounce(50),
|
||||
|
||||
|
||||
editIdentity = Identity => showScreenPopup(IdentityPopupView, [Identity]),
|
||||
|
||||
loadAccountsAndIdentities = () => {
|
||||
AccountUserStore.loading(true);
|
||||
IdentityUserStore.loading(true);
|
||||
|
|
@ -54,11 +48,6 @@ loadAccountsAndIdentities = () => {
|
|||
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
||||
: []
|
||||
);
|
||||
|
||||
// Invoke "Update Identity" pop up right after login
|
||||
// https://github.com/the-djmaze/snappymail/issues/1689
|
||||
const main = IdentityUserStore.main();
|
||||
main && !main.exists() && setTimeout(()=>editIdentity(main), 1000);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -132,7 +121,7 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
|||
next = 0,
|
||||
limit = 2;
|
||||
|
||||
if (1 < pageCount) {
|
||||
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
|
||||
if (pageCount < currentPage) {
|
||||
fAdd(pageCount);
|
||||
prev = pageCount;
|
||||
|
|
@ -370,5 +359,5 @@ populateMessageBody = (oMessage, popup) => {
|
|||
}
|
||||
};
|
||||
|
||||
leftPanelDisabled.subscribe(value => value && moveAction(0));
|
||||
leftPanelDisabled.subscribe(value => value && moveAction(false));
|
||||
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export class CheckboxComponent {
|
|||
: ko.observable(!!params.value);
|
||||
|
||||
this.enable = ko.isObservable(params.enable) ? params.enable
|
||||
: ko.observable(params.enable ?? 1);
|
||||
: ko.observable(undefined === params.enable || !!params.enable);
|
||||
|
||||
this.label = params.label;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export class JCard {
|
|||
// VCardProperty argument
|
||||
else if (arg instanceof VCardProperty) {
|
||||
let propArray = this.props.get(arg.getField());
|
||||
if (!propArray?.includes(arg))
|
||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
||||
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||
propArray.splice(propArray.indexOf(arg), 1);
|
||||
if (propArray.length === 0)
|
||||
|
|
|
|||
115
dev/External/SquireUI.js
vendored
115
dev/External/SquireUI.js
vendored
|
|
@ -3,6 +3,10 @@
|
|||
(doc => {
|
||||
|
||||
const
|
||||
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
|
||||
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
|
||||
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
|
||||
|
||||
i18n = (str, def) => rl.i18n(str) || def,
|
||||
|
||||
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
||||
|
|
@ -17,17 +21,38 @@ const
|
|||
|
||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||
|
||||
getFragmentOfChildren = parent => {
|
||||
let frag = doc.createDocumentFragment();
|
||||
frag.append(...parent.childNodes);
|
||||
return frag;
|
||||
},
|
||||
|
||||
SquireDefaultConfig = {
|
||||
/*
|
||||
addLinks: true // allow_smart_html_links
|
||||
*/
|
||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||
html = (html||'')
|
||||
tpl.innerHTML = (html||'')
|
||||
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
||||
.replace(/<!--[^>]+-->/g,'')
|
||||
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
||||
.trim();
|
||||
tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
|
||||
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||
if (isPaste) {
|
||||
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
|
||||
tpl.querySelectorAll('*').forEach(el => {
|
||||
if (!el.matches(allowedElements)) {
|
||||
el.replaceWith(getFragmentOfChildren(el));
|
||||
} else if (el.hasAttributes()) {
|
||||
[...el.attributes].forEach(attr => {
|
||||
let name = attr.name.toLowerCase();
|
||||
if (!allowedAttributes.includes(name)) {
|
||||
el.removeAttribute(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return tpl.content;
|
||||
}
|
||||
};
|
||||
|
|
@ -84,13 +109,13 @@ class SquireUI
|
|||
cmd: s => squire.setStyle({ fontFamily: s.value })
|
||||
},
|
||||
fontSize: {
|
||||
select: [[i18n('GLOBAL/DEFAULT'),''],'11px','13px','16px','20px','24px','30px'],
|
||||
defaultValueIndex: 0,
|
||||
select: ['11px','13px','16px','20px','24px','30px'],
|
||||
defaultValueIndex: 2,
|
||||
cmd: s => squire.setStyle({ fontSize: s.value })
|
||||
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
|
||||
// example:
|
||||
// select: ['','xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
||||
// defaultValueIndex: 0,
|
||||
// select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
||||
// defaultValueIndex: 3,
|
||||
},
|
||||
// dir: {
|
||||
// select: [
|
||||
|
|
@ -108,11 +133,11 @@ class SquireUI
|
|||
dir: {
|
||||
dir_ltr: {
|
||||
html: '⁋',
|
||||
cmd: () => squire.setTextDirection('ltr')
|
||||
cmd: () => squire.bidi('ltr')
|
||||
},
|
||||
dir_rtl: {
|
||||
html: '¶',
|
||||
cmd: () => squire.setTextDirection('rtl')
|
||||
cmd: () => squire.bidi('rtl')
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
|
|
@ -130,7 +155,7 @@ class SquireUI
|
|||
html: 'B',
|
||||
cmd: () => this.doAction('bold'),
|
||||
key: 'B',
|
||||
matches: 'B,STRONG'
|
||||
matches: 'B,STRONT'
|
||||
},
|
||||
italic: {
|
||||
html: 'I',
|
||||
|
|
@ -212,7 +237,7 @@ class SquireUI
|
|||
cmd: () => {
|
||||
let node = squire.getSelectionClosest('IMG'),
|
||||
src = prompt("Image", node?.src || "https://");
|
||||
src?.length ? squire.insertImage(src) : node?.remove();
|
||||
src?.length ? squire.insertImage(src) : (node && squire.detach(node));
|
||||
},
|
||||
matches: 'IMG'
|
||||
},
|
||||
|
|
@ -330,7 +355,6 @@ class SquireUI
|
|||
input.append(option);
|
||||
});
|
||||
} else {
|
||||
input.add(new Option(i18n('GLOBAL/DEFAULT'), ''));
|
||||
Object.entries(cfg.select).forEach(([label, options]) => {
|
||||
let group = createElement('optgroup');
|
||||
group.label = label;
|
||||
|
|
@ -339,7 +363,7 @@ class SquireUI
|
|||
option.style[action] = value;
|
||||
group.append(option);
|
||||
});
|
||||
input.add(group);
|
||||
input.append(group);
|
||||
});
|
||||
}
|
||||
ev = 'input';
|
||||
|
|
@ -381,47 +405,9 @@ class SquireUI
|
|||
|
||||
let changes = actions.changes;
|
||||
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
||||
squire.addEventListener('undoStateChange', e => {
|
||||
changes.undo.input.disabled = !e.detail.canUndo;
|
||||
changes.redo.input.disabled = !e.detail.canRedo;
|
||||
});
|
||||
|
||||
squire.addEventListener('pasteImage', e => {
|
||||
const items = e.detail.clipboardData.items;
|
||||
let l = items.length;
|
||||
while (l--) {
|
||||
const item = items[l];
|
||||
if (/^image\/(png|jpeg|webp)/.test(item.type)) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
let img = createElement("img"),
|
||||
canvas = createElement("canvas"),
|
||||
ctx = canvas.getContext('2d');
|
||||
img.onload = ()=>{
|
||||
ctx.drawImage(img, 0, 0);
|
||||
let width = img.width, height = img.height;
|
||||
if (width > height) {
|
||||
// Landscape
|
||||
if (width > 1024) {
|
||||
height = height * 1024 / width;
|
||||
width = 1024;
|
||||
}
|
||||
} else if (height > 1024) {
|
||||
// Portrait
|
||||
width = width * 1024 / height;
|
||||
height = 1024;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
squire.insertHTML('<img alt="" style="width:100%;max-width:'+width+'px" src="'+canvas.toDataURL()+'">', true);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
}
|
||||
reader.readAsDataURL(item.getAsFile());
|
||||
break;
|
||||
}
|
||||
}
|
||||
squire.addEventListener('undoStateChange', state => {
|
||||
changes.undo.input.disabled = !state.canUndo;
|
||||
changes.redo.input.disabled = !state.canRedo;
|
||||
});
|
||||
|
||||
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||
|
|
@ -491,31 +477,24 @@ class SquireUI
|
|||
|
||||
// -----
|
||||
|
||||
squire.addEventListener('pathChange', () => {
|
||||
squire.addEventListener('pathChange', e => {
|
||||
|
||||
const squireRoot = squire.getRoot();
|
||||
let range = squire.getSelection(),
|
||||
collapsed = range.collapsed,
|
||||
elm = collapsed ? range.endContainer : range?.commonAncestorContainer;
|
||||
if (elm && !(elm instanceof Element)) {
|
||||
elm = elm.parentElement;
|
||||
}
|
||||
|
||||
forEachObjectValue(actions, entries => {
|
||||
forEachObjectValue(entries, cfg => {
|
||||
// Check if selection has a matching parent or contains a matching element
|
||||
cfg.matches && cfg.input.classList.toggle('active', !!(elm && (
|
||||
(!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node)))
|
||||
|| elm.closestWithin(cfg.matches, squireRoot)
|
||||
)));
|
||||
// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches));
|
||||
cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot));
|
||||
});
|
||||
});
|
||||
|
||||
if (elm) {
|
||||
if (e.element) {
|
||||
// try to find font-family and/or font-size and set "select" elements' values
|
||||
|
||||
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||
let familySelectedIndex = defaultFontFamilyIndex;
|
||||
|
||||
let elm = e.element;
|
||||
let familyFound = false;
|
||||
let sizeFound = false;
|
||||
do {
|
||||
|
|
@ -547,10 +526,10 @@ class SquireUI
|
|||
});
|
||||
/*
|
||||
squire.addEventListener('cursor', e => {
|
||||
console.dir({cursor:e.detail.range});
|
||||
console.dir({cursor:e.range});
|
||||
});
|
||||
squire.addEventListener('select', e => {
|
||||
console.dir({select:e.detail.range});
|
||||
console.dir({select:e.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);
|
||||
|
||||
// ko.addDisposeCallback(element, () => {
|
||||
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
dev/External/ko.js
vendored
7
dev/External/ko.js
vendored
|
|
@ -15,7 +15,6 @@ export const
|
|||
* based solely on the values of other observables in the application
|
||||
*/
|
||||
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||
// koObservable = value => ko.observable(value),
|
||||
|
||||
addObservablesTo = (target, observables) =>
|
||||
forEachObjectEntry(observables, (key, value) =>
|
||||
|
|
@ -31,7 +30,7 @@ export const
|
|||
|
||||
onEvent = (element, event, fn) => {
|
||||
element.addEventListener(event, fn);
|
||||
ko.addDisposeCallback(element, () => element.removeEventListener(event, fn));
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
|
||||
},
|
||||
|
||||
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
||||
|
|
@ -176,6 +175,6 @@ ko.extenders.falseTimeout = (target, option) => {
|
|||
|
||||
// functions
|
||||
|
||||
ko.observable.fn.askDeleteHelper = function(prop='askDelete') {
|
||||
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, prop] });
|
||||
ko.observable.fn.askDeleteHelper = function() {
|
||||
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
|
||||
};
|
||||
|
|
|
|||
55
dev/Html/PreviewMessage.html
Normal file
55
dev/Html/PreviewMessage.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<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,9 +60,10 @@ export class AbstractViewPopup extends AbstractView
|
|||
this.keyScope.scope = name;
|
||||
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||
this.close = () => this.modalVisible(false);
|
||||
this.tryToClose = () => (false === this.onClose()) || this.close();
|
||||
addShortcut('escape,close', '', name, () => {
|
||||
this.modalVisible() && this.tryToClose();
|
||||
if (this.modalVisible() && false !== this.onClose()) {
|
||||
this.close();
|
||||
}
|
||||
return false;
|
||||
// return true; Issue with supported modal close
|
||||
});
|
||||
|
|
@ -115,11 +116,6 @@ export class AbstractViewSettings
|
|||
onHide() {}
|
||||
viewModelDom
|
||||
*/
|
||||
/**
|
||||
* When this[name] does not exists, create as observable with value of SettingsGet(name)
|
||||
* When this[name+'Trigger'] does not exists, create as observable
|
||||
* Subscribe to this[name], and handle saving the setting
|
||||
*/
|
||||
addSetting(name, valueCb)
|
||||
{
|
||||
let prop = name[0].toLowerCase() + name.slice(1),
|
||||
|
|
@ -135,7 +131,6 @@ export class AbstractViewSettings
|
|||
rl.app.Remote.saveSetting(name, value,
|
||||
iError => {
|
||||
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
|
||||
// iError || Settings.set(name, value);
|
||||
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
|
||||
}
|
||||
);
|
||||
|
|
@ -143,10 +138,6 @@ export class AbstractViewSettings
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Foreach name if this[name] does not exists, create as observable with value of SettingsGet(name)
|
||||
* Subscribe to this[name], for saving the setting
|
||||
*/
|
||||
addSettings(names)
|
||||
{
|
||||
names.forEach(name => {
|
||||
|
|
|
|||
|
|
@ -74,17 +74,13 @@ const
|
|||
// vmDom.addEventListener('close', () => vm.modalVisible(false));
|
||||
|
||||
// show/hide popup/modal
|
||||
// transitionend is called for each property, so we only listen to `opacity`
|
||||
// as defined in CSS by `dialog:not(.animate)`
|
||||
const endShowHide = e => {
|
||||
if (e.target === vmDom && 'opacity' === e.propertyName) {
|
||||
if (e.target === vmDom) {
|
||||
if (vmDom.classList.contains('animate')) {
|
||||
vm.afterShow?.();
|
||||
fireEvent('rl-vm-visible', vm);
|
||||
} else {
|
||||
vmDom.close();
|
||||
vm.afterHide?.();
|
||||
// fireEvent('rl-vm-hidden', vm);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -167,7 +163,7 @@ const
|
|||
if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) {
|
||||
// Close all popups
|
||||
for (let vm of visiblePopups) {
|
||||
vm.tryToClose();
|
||||
(false === vm.onClose()) || vm.close();
|
||||
}
|
||||
|
||||
let vmScreen = screen(screenName);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function ParseMime(text)
|
|||
get body() {
|
||||
let body = this.bodyRaw,
|
||||
charset = this.header('content-type')?.params.charset,
|
||||
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||
encoding = this.headerValue('content-transfer-encoding');
|
||||
if ('quoted-printable' == encoding) {
|
||||
body = QPDecode(body);
|
||||
} else if ('base64' == encoding) {
|
||||
|
|
@ -94,7 +94,7 @@ export function ParseMime(text)
|
|||
|
||||
get dataUrl() {
|
||||
let body = this.bodyRaw,
|
||||
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||
encoding = this.headerValue('content-transfer-encoding');
|
||||
if ('base64' == encoding) {
|
||||
body = body.replace(/\r?\n/g, '');
|
||||
} else {
|
||||
|
|
@ -112,7 +112,7 @@ export function ParseMime(text)
|
|||
}
|
||||
|
||||
getByContentType(type) {
|
||||
if (type == this.headerValue('content-type')?.toLowerCase()) {
|
||||
if (type == this.headerValue('content-type')) {
|
||||
return this;
|
||||
}
|
||||
let i = 0, p = this.parts, part;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export function MimeToMessage(data, message)
|
|||
subject = struct.headerValue('subject');
|
||||
html = html ? html.body : '';
|
||||
|
||||
// Content-Type: ...; protected-headers="v1"
|
||||
subject && message.subject(subject);
|
||||
|
||||
// EmailCollectionModel
|
||||
|
|
@ -100,4 +99,6 @@ export function MimeToMessage(data, message)
|
|||
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
||||
message.pgpSigned(true);
|
||||
}
|
||||
|
||||
// TODO: Verify instantly?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { baseCollator } from 'Common/Translator';
|
||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
import { AttachmentModel } from 'Model/Attachment';
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
|||
*/
|
||||
static reviveFromJson(items) {
|
||||
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||
let collator = baseCollator(true);
|
||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
attachments.sort((a, b) => {
|
||||
if (a.isInline()) {
|
||||
if (!b.isInline()) {
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ export class ContactModel extends AbstractModel {
|
|||
this.namePrefix(),
|
||||
this.nameSuffix()
|
||||
]/*, params, group*/);
|
||||
jCard.parseFullName({set:true});
|
||||
// jCard.parseFullName({set:true});
|
||||
|
||||
['nickname', 'note', 'title'].forEach(field =>
|
||||
this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field)
|
||||
|
|
|
|||
|
|
@ -73,9 +73,4 @@ export class EmailModel extends AbstractModel {
|
|||
}
|
||||
return result || name;
|
||||
}
|
||||
|
||||
get domain() {
|
||||
return this.email.replace(/^.+@([^@]+)$/, '$1');
|
||||
// return this.email.slice(this.email.lastIndexOf('@'))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ import { FileInfo } from 'Common/File';
|
|||
import { FolderPopupView } from 'View/Popup/Folder';
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { isAllowedKeyword } from 'Stores/User/Folder';
|
||||
|
||||
const
|
||||
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||
|
||||
|
|
@ -104,7 +102,7 @@ export const
|
|||
// Repeat every 15 minutes?
|
||||
// this.foldersTimeout = setTimeout(loadFolders, 900000);
|
||||
})
|
||||
.catch(e => fCallback && setTimeout(fCallback, 1, false, e));
|
||||
.catch(() => fCallback && setTimeout(fCallback, 1, false));
|
||||
};
|
||||
|
||||
export class FolderCollectionModel extends AbstractCollectionModel
|
||||
|
|
@ -413,8 +411,6 @@ export class FolderModel extends AbstractModel {
|
|||
canBeSubscribed: () => this.selectable()
|
||||
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
||||
|
||||
optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
|
||||
|
||||
/**
|
||||
* Folder is visible when:
|
||||
* - hasVisibleSubfolders()
|
||||
|
|
@ -471,25 +467,16 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
detailedName: () => this.name() + ' ' + this.nameInfo(),
|
||||
|
||||
icon: () => {
|
||||
switch (this.type())
|
||||
{
|
||||
case 1: return '📥'; // FolderType.Inbox
|
||||
case 2: return '📧'; // FolderType.Sent icon-paper-plane
|
||||
case 3: return '🗎'; // FolderType.Drafts
|
||||
case 4: return '⚠'; // FolderType.Junk
|
||||
case 5: return '🗑'; // FolderType.Trash
|
||||
case 6: return '🗄'; // FolderType.Archive
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
hasUnreadInSub: () =>
|
||||
this.subFolders().some(
|
||||
folder => folder.unreadEmails() | folder.hasUnreadInSub()
|
||||
),
|
||||
|
||||
href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
||||
!!this.subFolders().find(
|
||||
folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||
)
|
||||
/*
|
||||
!!this.subFolders().filter(
|
||||
folder => folder.unreadEmails() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||
).length
|
||||
*/
|
||||
,href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
//import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
export class IdentityModel extends EmailModel /*AbstractModel*/ {
|
||||
export class IdentityModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {string} email
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
addObservablesTo(this, {
|
||||
id: '',
|
||||
label: '',
|
||||
// email: '',
|
||||
// name: '',
|
||||
email: '',
|
||||
name: '',
|
||||
|
||||
replyTo: '',
|
||||
bcc: '',
|
||||
|
|
@ -24,28 +27,23 @@ export class IdentityModel extends EmailModel /*AbstractModel*/ {
|
|||
|
||||
smimeKey: '',
|
||||
smimeCertificate: '',
|
||||
smimeCertificateChain: '',
|
||||
|
||||
askDelete: false,
|
||||
|
||||
exists: false
|
||||
askDelete: false
|
||||
});
|
||||
|
||||
addComputablesTo(this, {
|
||||
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
|
||||
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
|
||||
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()),
|
||||
smimeCertificateChainValid: () => !this.smimeCertificateChain()
|
||||
|| /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificateChain())
|
||||
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()),
|
||||
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
const name = this.name,
|
||||
email = this.email,
|
||||
formattedName() {
|
||||
const name = this.name(),
|
||||
email = this.email(),
|
||||
label = this.label();
|
||||
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
|||
import { serverRequestRaw, proxy } from 'Common/Links';
|
||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { FileInfo, RFC822 } from 'Common/File';
|
||||
|
|
@ -18,73 +18,15 @@ import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection';
|
|||
//import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { MimeToMessage } from 'Mime/Utils';
|
||||
|
||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { Passphrases } from 'Storage/Passphrases';
|
||||
|
||||
const
|
||||
PreviewHTML = `<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),
|
||||
|
||||
toggleTag = (message, keyword) => {
|
||||
|
|
@ -139,6 +81,7 @@ export class MessageModel extends AbstractModel {
|
|||
// autocrypt: ko.observableArray(),
|
||||
hasVirus: null, // or boolean when scanned
|
||||
priority: 3, // Normal
|
||||
internalTimestamp: 0,
|
||||
senderEmailsString: '',
|
||||
senderClearEmailsString: '',
|
||||
isSpam: false,
|
||||
|
|
@ -161,7 +104,8 @@ export class MessageModel extends AbstractModel {
|
|||
plain: '',
|
||||
html: '',
|
||||
dateTimestamp: 0,
|
||||
dateTimestampSource: 0,
|
||||
|
||||
deleted: false,
|
||||
|
||||
// Also used by Selector
|
||||
focused: false,
|
||||
|
|
@ -208,21 +152,22 @@ export class MessageModel extends AbstractModel {
|
|||
|
||||
isUnseen: () => !this.flags().includes('\\seen'),
|
||||
isFlagged: () => this.flags().includes('\\flagged'),
|
||||
isDeleted: () => this.flags().includes('\\deleted'),
|
||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||
// isPhishing: () => this.flags().includes('$phishing'),
|
||||
|
||||
tagOptions: () => {
|
||||
const tagOptions = [];
|
||||
FolderUserStore.currentFolder().optionalTags().forEach(value => {
|
||||
let lower = value.toLowerCase();
|
||||
tagOptions.push({
|
||||
css: 'msgflag-' + lower,
|
||||
value: value,
|
||||
checked: this.flags().includes(lower),
|
||||
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
|
||||
toggle: (/*obj*/) => toggleTag(this, value)
|
||||
});
|
||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
||||
if (isAllowedKeyword(value)) {
|
||||
let lower = value.toLowerCase();
|
||||
tagOptions.push({
|
||||
css: 'msgflag-' + lower,
|
||||
value: value,
|
||||
checked: this.flags().includes(lower),
|
||||
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
|
||||
toggle: (/*obj*/) => toggleTag(this, value)
|
||||
});
|
||||
}
|
||||
});
|
||||
return tagOptions
|
||||
},
|
||||
|
|
@ -366,6 +311,7 @@ export class MessageModel extends AbstractModel {
|
|||
lineAsCss(flags=1) {
|
||||
let classes = [];
|
||||
forEachObjectEntry({
|
||||
deleted: this.deleted(),
|
||||
selected: this.selected(),
|
||||
checked: this.checked(),
|
||||
unseen: this.isUnseen(),
|
||||
|
|
@ -567,7 +513,7 @@ export class MessageModel extends AbstractModel {
|
|||
hasImages = true;
|
||||
},
|
||||
attr = 'data-x-src',
|
||||
src, useProxy = !!SettingsGet('proxyExternalImages');
|
||||
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
|
||||
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
||||
src = node.getAttribute(attr);
|
||||
if (isValid(src)) {
|
||||
|
|
@ -602,140 +548,4 @@ export class MessageModel extends AbstractModel {
|
|||
return result.html || plainToHtml(this.plain());
|
||||
}
|
||||
|
||||
async decrypt() {
|
||||
const msg = this;
|
||||
if (msg.pgpEncrypted() && !msg.pgpDecrypted()) {
|
||||
return await msg.pgpDecrypt().then(()=>msg.pgpDecrypted());
|
||||
}
|
||||
if (msg.smimeEncrypted() && !msg.smimeDecrypted()) {
|
||||
return await msg.smimeDecrypt().then(()=>msg.smimeDecrypted());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
async decrypted() {
|
||||
return msg.pgpDecrypted() || msg.smimeDecrypted());
|
||||
}
|
||||
*/
|
||||
async pgpDecrypt() {
|
||||
const oMessage = this,
|
||||
data = oMessage.pgpEncrypted();
|
||||
delete data.error;
|
||||
await PgpUserStore.decrypt(oMessage).then(result => {
|
||||
if (!result) {
|
||||
// TODO: translate
|
||||
throw Error('Decryption failed, canceled or not possible');
|
||||
}
|
||||
oMessage.pgpDecrypted(true);
|
||||
if (result.data) {
|
||||
MimeToMessage(result.data, oMessage);
|
||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||
if (result.signatures?.length) {
|
||||
oMessage.pgpSigned({
|
||||
signatures: result.signatures,
|
||||
success: !!result.signatures.length
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
data.error = e.message;
|
||||
})
|
||||
.finally(() => {
|
||||
oMessage.pgpEncrypted(data);
|
||||
});
|
||||
}
|
||||
|
||||
pgpVerify(/*self, event*/) {
|
||||
const oMessage = this;
|
||||
PgpUserStore.verify(oMessage).then(result => {
|
||||
if (result) {
|
||||
oMessage.pgpSigned(result);
|
||||
} else {
|
||||
alert('Verification failed or no valid public key found');
|
||||
}
|
||||
/*
|
||||
if (result?.success) {
|
||||
i18n('CRYPTO/GOOD_SIGNATURE', {
|
||||
USER: validKey.user + ' (' + validKey.id + ')'
|
||||
});
|
||||
message.getText()
|
||||
} else {
|
||||
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
|
||||
additional = keyIds
|
||||
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
|
||||
: '';
|
||||
|
||||
i18n('CRYPTO/ERROR', {
|
||||
TYPE: 'OpenPGP',
|
||||
ERROR: 'message'
|
||||
}) + (additional ? ' (' + additional + ')' : '');
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
async smimeDecrypt() {
|
||||
const message = this;
|
||||
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
|
||||
identity = IdentityUserStore.find(item => addresses.includes(item.email)),
|
||||
data = message.smimeEncrypted(); // { partId: "1" }
|
||||
if (data && identity) {
|
||||
delete data.error;
|
||||
let pass, params = { ...data }; // clone
|
||||
params.folder = message.folder;
|
||||
params.uid = message.uid;
|
||||
// params.bodyPart = params.bodyPart?.raw;
|
||||
params.certificate = identity.smimeCertificate();
|
||||
params.privateKey = identity.smimeKey();
|
||||
if (identity.smimeKeyEncrypted()) {
|
||||
pass = await Passphrases.ask(identity,
|
||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
|
||||
'CRYPTO/DECRYPT'
|
||||
);
|
||||
if (!pass) {
|
||||
return;
|
||||
}
|
||||
params.passphrase = pass?.password;
|
||||
}
|
||||
await Remote.post('SMimeDecryptMessage', null, params).then(response => {
|
||||
if (response?.Result?.data) {
|
||||
message.smimeDecrypted(true);
|
||||
MimeToMessage(response.Result.data, message);
|
||||
message.html() ? message.viewHtml() : message.viewPlain();
|
||||
pass && pass.remember && Passphrases.handle(identity, pass.password);
|
||||
if ('signed' in response.Result) {
|
||||
message.smimeSigned(response.Result.signed);
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
data.error = e.message
|
||||
})
|
||||
.finally(() => {
|
||||
message.smimeEncrypted(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
smimeVerify(/*self, event*/) {
|
||||
const message = this,
|
||||
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||
if (data) {
|
||||
const params = { ...data }; // clone
|
||||
params.folder = message.folder;
|
||||
params.uid = message.uid;
|
||||
params.bodyPart = data.bodyPart?.raw;
|
||||
params.sigPart = data.sigPart?.bodyRaw;
|
||||
Remote.post('SMimeVerifyMessage', null, params).then(response => {
|
||||
if (response?.Result) {
|
||||
if (response.Result.body) {
|
||||
MimeToMessage(response.Result.body, message);
|
||||
message.html() ? message.viewHtml() : message.viewPlain();
|
||||
}
|
||||
data.success = response.Result.success;
|
||||
message.smimeSigned(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,12 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
let msg = MessageUserStore.message();
|
||||
return super.reviveFromJson(object, message => {
|
||||
// If message is currently viewed, use that.
|
||||
if (msg && msg.hash === message.hash) {
|
||||
msg.revivePropertiesFromJson(message);
|
||||
message = msg;
|
||||
} else {
|
||||
message = MessageModel.reviveFromJson(message);
|
||||
// Maybe then use msg.revivePropertiesFromJson(message) ?
|
||||
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
|
||||
if (message) {
|
||||
message.deleted(false);
|
||||
return message;
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,20 +10,18 @@ export class MimeHeaderAutocryptModel/* extends AbstractModel*/
|
|||
|
||||
if (value) {
|
||||
value.split(';').forEach(entry => {
|
||||
entry = entry.match(/^([^=]+)=(.*)$/);
|
||||
entry = entry.split('=');
|
||||
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, '');
|
||||
this[trim(entry[1]).replace('-', '_')] = trim(entry[2]);
|
||||
this[trim(entry[0]).replace('-', '_')] = trim(entry[1]);
|
||||
});
|
||||
this.keydata = this.keydata.replace(/\s+/g, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
let result = `addr=${this.addr}; `;
|
||||
if ('mutual' === this.prefer_encrypt) {
|
||||
result += 'prefer-encrypt=mutual; ';
|
||||
return `addr=${this.addr}; prefer-encrypt=mutual; keydata=${this.keydata}`;
|
||||
}
|
||||
return result + 'keydata=' + this.keydata.replace(/\n/g, '\n ');
|
||||
return `addr=${this.addr}; keydata=${this.keydata}`;
|
||||
}
|
||||
|
||||
pem() {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ let iJsonErrorCount = 0;
|
|||
const getURL = (add = '') => serverRequest('Json') + pString(add),
|
||||
|
||||
checkResponseError = data => {
|
||||
const err = data ? data.code : null;
|
||||
const err = data ? data.ErrorCode : null;
|
||||
if (Notifications.InvalidToken === err) {
|
||||
console.error(getNotification(err) + ` (${data.messageAdditional})`);
|
||||
console.error(getNotification(err));
|
||||
// alert(getNotification(err));
|
||||
setTimeout(rl.logoutReload, 5000);
|
||||
rl.logoutReload();
|
||||
} else if ([
|
||||
Notifications.AuthError,
|
||||
Notifications.ConnectionError,
|
||||
|
|
@ -131,7 +131,7 @@ export class AbstractFetchRemote
|
|||
|
||||
fetchJSON(sAction, getURL(sGetAdd),
|
||||
sGetAdd ? null : (params || {}),
|
||||
pInt(iTimeout ?? 30000),
|
||||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
||||
async data => {
|
||||
let iError = 0;
|
||||
if (data) {
|
||||
|
|
@ -144,7 +144,7 @@ export class AbstractFetchRemote
|
|||
iJsonErrorCount = 0;
|
||||
} else {
|
||||
checkResponseError(data);
|
||||
iError = data.code || Notifications.UnknownError
|
||||
iError = data.ErrorCode || Notifications.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ export class AbstractFetchRemote
|
|||
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||
}
|
||||
|
||||
if (111 === data?.code && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||
if (111 === data?.ErrorCode && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||
return this.post(action, fTrigger, params, timeOut);
|
||||
}
|
||||
/*
|
||||
|
|
@ -222,8 +222,8 @@ export class AbstractFetchRemote
|
|||
if (!data.Result || action !== data.Action) {
|
||||
checkResponseError(data);
|
||||
return Promise.reject(new FetchError(
|
||||
data ? data.code : 0,
|
||||
data ? (data.messageAdditional || data.message) : ''
|
||||
data ? data.ErrorCode : 0,
|
||||
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
rules = {
|
||||
subname: /^(.*)$/,
|
||||
normalize_: (rquest, vals) => {
|
||||
vals.subname = pString(vals.subname ?? defaultRoute);
|
||||
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
|
||||
return [vals.subname];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
*/
|
||||
onBuild() {
|
||||
doc.addEventListener('click', event =>
|
||||
event.target.closest('#rl-right') && moveAction(0)
|
||||
event.target.closest('#rl-right') && moveAction(false)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
|||
coreWarning: false,
|
||||
coreVersion: '',
|
||||
coreVersionCompare: -2,
|
||||
php64: true,
|
||||
load1: 0,
|
||||
load5: 0,
|
||||
load15: 0,
|
||||
|
|
@ -70,7 +69,6 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
|||
this.coreWarning(!!data.core.warning);
|
||||
this.coreVersion(data.core.version || '');
|
||||
this.coreVersionCompare(data.core.versionCompare);
|
||||
this.php64(data.php[1].loaded);
|
||||
} else {
|
||||
this.coreReal(false);
|
||||
this.coreWarning(false);
|
||||
|
|
@ -80,10 +78,6 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
|||
});
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
Remote.request('AdminClearCache');
|
||||
}
|
||||
|
||||
updateCoreData() {
|
||||
if (!this.coreUpdating()) {
|
||||
this.coreUpdating(true);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
|||
|
||||
constructor() {
|
||||
this.config = ko.observableArray();
|
||||
this.search = ko.observable('');
|
||||
this.search = ko.observableArray();
|
||||
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
||||
|
||||
this.search.subscribe(value => {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
|||
testUsername() {
|
||||
Remote.request('AdminDomainMatch',
|
||||
(iError, oData) => {
|
||||
if (oData?.Result?.name) {
|
||||
alert(`${oData.Result.email} matched domain: ${oData.Result.name}`);
|
||||
if (oData?.Result?.domain) {
|
||||
alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`);
|
||||
} else {
|
||||
alert('No domain match');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
this.addSettings(['pluginsEnable']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
packagesError: '',
|
||||
search: ''
|
||||
packagesError: ''
|
||||
});
|
||||
|
||||
this.packages = PackageAdminStore;
|
||||
|
|
@ -31,19 +30,6 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
|
||||
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
|
||||
});
|
||||
|
||||
this.search.subscribe(value => {
|
||||
const v = value.toLowerCase(),
|
||||
qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
|
||||
match = node => node.textContent.toLowerCase().includes(v);
|
||||
if (v.length) {
|
||||
qsa(this.viewModelDom, 'td:first-child', td => {
|
||||
td.parentNode.hidden = !match(td);
|
||||
});
|
||||
} else {
|
||||
qsa(this.viewModelDom, 'tr[hidden]', n => n.hidden = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onShow() {
|
||||
|
|
@ -82,7 +68,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
if (iError) {
|
||||
this.packagesError(
|
||||
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
|
||||
+ (data.message ? ':\n' + data.message : '')
|
||||
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
||||
);
|
||||
} else if (data.Result.Reload) {
|
||||
location.reload();
|
||||
|
|
@ -127,8 +113,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
if (iError) {
|
||||
plugin.enabled(disable);
|
||||
this.packagesError(
|
||||
(Notifications.UnsupportedPluginPackage === iError && data?.message)
|
||||
? data.message
|
||||
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
||||
? data.ErrorMessage
|
||||
: getNotification(iError)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);
|
||||
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
|
||||
|
||||
this.weakPassword = rl.app.weakPassword;
|
||||
|
||||
|
|
@ -32,8 +32,6 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
capaOpenPGP: SettingsCapa('OpenPGP')
|
||||
});
|
||||
|
||||
this.gnuPGversion = 'GnuPG v' + SettingsGet('gnupg');
|
||||
|
||||
const reset = () => {
|
||||
this.saveError(false);
|
||||
this.saveSuccess(false);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
|||
|
||||
//import { koComputable } from 'External/ko';
|
||||
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||
import { loadAccountsAndIdentities, editIdentity } from 'Common/UtilsUser';
|
||||
import { loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
|
|
@ -12,6 +12,7 @@ import Remote from 'Remote/User/Fetch';
|
|||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { AccountPopupView } from 'View/Popup/Account';
|
||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||
|
||||
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||
constructor() {
|
||||
|
|
@ -43,11 +44,11 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
}
|
||||
|
||||
addNewIdentity() {
|
||||
editIdentity();
|
||||
showScreenPopup(IdentityPopupView);
|
||||
}
|
||||
|
||||
editIdentity(identity) {
|
||||
editIdentity(identity);
|
||||
showScreenPopup(IdentityPopupView, [identity]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -88,7 +89,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
|
||||
accountsAndIdentitiesAfterMove() {
|
||||
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
||||
Accounts: AccountUserStore.filter(item => item.isAdditional()).map(item => item.email),
|
||||
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
|
||||
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
|||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { i18n, translateTrigger, getErrorMessage } from 'Common/Translator';
|
||||
import { i18n, translateTrigger } from 'Common/Translator';
|
||||
import { ContactUserStore } from 'Stores/User/Contact';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
|
|
@ -15,7 +15,6 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
|
|||
this.syncUrl = ContactUserStore.syncUrl;
|
||||
this.syncUser = ContactUserStore.syncUser;
|
||||
this.syncPass = ContactUserStore.syncPass;
|
||||
this.syncError = ko.observable('');
|
||||
|
||||
this.syncModeOptions = koComputable(() => {
|
||||
translateTrigger();
|
||||
|
|
@ -49,15 +48,4 @@ 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,16 +6,15 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
|||
import { setRefreshFoldersInterval } from 'Common/Folders';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { WYSIWYGS } from 'Common/HtmlEditor';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
|
||||
import { editIdentity } from 'Common/UtilsUser';
|
||||
|
||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||
|
|
@ -23,14 +22,13 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||
import { LanguagesPopupView } from 'View/Popup/Languages';
|
||||
|
||||
export class UserSettingsGeneral extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.mailto = ko.observable(!!navigator.registerProtocolHandler);
|
||||
|
||||
this.language = LanguageStore.language;
|
||||
this.languages = LanguageStore.languages;
|
||||
this.hourCycle = LanguageStore.hourCycle;
|
||||
|
|
@ -39,74 +37,21 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
|
||||
|
||||
this.minRefreshInterval = SettingsGet('minRefreshInterval');
|
||||
|
||||
this.desktopNotifications = NotificationUserStore.enabled;
|
||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||
|
||||
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')
|
||||
}
|
||||
];
|
||||
|
||||
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',
|
||||
['useThreads',
|
||||
// These use addSetting()
|
||||
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'minFontSize',
|
||||
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
|
||||
// These are in addSettings()
|
||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
|
||||
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
|
||||
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown'
|
||||
'messageReadAuto', 'showNextMessage', 'messageNewWindow'
|
||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
||||
|
|
@ -120,9 +65,14 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
addComputablesTo(this, {
|
||||
languageFullName: () => convertLangName(this.language()),
|
||||
|
||||
identityMain: () => {
|
||||
const list = this.identities();
|
||||
return isArray(list) ? list.find(item => item && !item.id()) : null;
|
||||
},
|
||||
|
||||
identityMainDesc: () => {
|
||||
const identity = IdentityUserStore.main();
|
||||
return identity ? identity.toString() : '---';
|
||||
const identity = this.identityMain();
|
||||
return identity ? identity.formattedName() : '---';
|
||||
},
|
||||
|
||||
editorDefaultTypes: () => {
|
||||
|
|
@ -161,15 +111,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.addSetting('CheckMailInterval');
|
||||
this.addSetting('Layout');
|
||||
this.addSetting('MaxBlockquotesLevel');
|
||||
this.addSetting('minFontSize');
|
||||
this.addSetting('defaultSort');
|
||||
|
||||
this.addSettings([
|
||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
|
||||
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
|
||||
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
|
||||
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown',
|
||||
'messageReadAuto', 'showNextMessage', 'messageNewWindow',
|
||||
'DesktopNotifications', 'SoundNotification']);
|
||||
|
||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||
|
|
@ -198,11 +146,6 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
Remote.saveSetting('UseThreads', value);
|
||||
},
|
||||
|
||||
threadAlgorithm: value => {
|
||||
MessagelistUserStore([]);
|
||||
Remote.saveSetting('threadAlgorithm', value);
|
||||
},
|
||||
|
||||
checkMailInterval: () => {
|
||||
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
||||
}
|
||||
|
|
@ -210,7 +153,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
}
|
||||
|
||||
editMainIdentity() {
|
||||
editIdentity(IdentityUserStore.main());
|
||||
const identity = this.identityMain();
|
||||
identity && showScreenPopup(IdentityPopupView, [identity]);
|
||||
}
|
||||
|
||||
testSoundNotification() {
|
||||
|
|
@ -224,15 +168,4 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
selectLanguage() {
|
||||
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
|
||||
}
|
||||
|
||||
registerMailto() {
|
||||
console.log(`mailto = ${location.protocol}//${location.host}${location.pathname}?mailto`);
|
||||
navigator.registerProtocolHandler(
|
||||
'mailto',
|
||||
`${location.protocol}//${location.host}${location.pathname}?mailto&to=%s`,
|
||||
(SettingsGet('title') || 'SnappyMail')
|
||||
);
|
||||
alert(i18n('GLOBAL/DONE'));
|
||||
this.mailto(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
|||
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -26,9 +28,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
this.autoLogoutOptions = koComputable(() => {
|
||||
translateTrigger();
|
||||
return [
|
||||
{ id: 0, name: i18n('SETTINGS_SECURITY/NEVER') },
|
||||
{ id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') },
|
||||
{ id: 5, name: relativeTime(300) },
|
||||
{ id: 15, name: relativeTime(900) },
|
||||
{ id: 10, name: relativeTime(600) },
|
||||
{ id: 30, name: relativeTime(1800) },
|
||||
{ id: 60, name: relativeTime(3600) },
|
||||
{ id: 120, name: relativeTime(7200) },
|
||||
|
|
@ -38,9 +40,6 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
});
|
||||
this.addSetting('AutoLogout');
|
||||
|
||||
this.keyPassForget = SettingsUserStore.keyPassForget;
|
||||
this.addSetting('keyPassForget');
|
||||
|
||||
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
|
||||
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
|
||||
|
||||
|
|
@ -63,7 +62,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
}
|
||||
|
||||
importToOpenPGP() {
|
||||
OpenPGPUserStore.loadBackupKeys();
|
||||
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
|
||||
(iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
|
||||
);
|
||||
}
|
||||
|
||||
importToSMime() {
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
|||
themeBackground.hash(data?.Result?.hash || '');
|
||||
if (!themeBackground.name() || !themeBackground.hash()) {
|
||||
let errorMsg = '';
|
||||
if (data.code) {
|
||||
switch (data.code) {
|
||||
if (data.ErrorCode) {
|
||||
switch (data.ErrorCode) {
|
||||
case UploadErrorCode.FileIsTooBig:
|
||||
errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG');
|
||||
break;
|
||||
|
|
@ -111,7 +111,7 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
|||
}
|
||||
}
|
||||
|
||||
themeBackground.error(errorMsg || data.message || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
|
||||
themeBackground.error(errorMsg || data.ErrorMessage || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from 'Sieve/Commands/Actions';
|
||||
|
||||
import {
|
||||
ConditionalCommand,
|
||||
ElsIfCommand,
|
||||
ElseCommand,
|
||||
IfCommand,
|
||||
|
|
@ -48,7 +49,6 @@ import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from
|
|||
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
|
||||
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
|
||||
import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703';
|
||||
import { ValidExtListTest } from 'Sieve/Extensions/rfc6134';
|
||||
import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609';
|
||||
|
||||
export const
|
||||
|
|
@ -67,6 +67,7 @@ export const
|
|||
IfCommand,
|
||||
ElsIfCommand,
|
||||
ElseCommand,
|
||||
ConditionalCommand,
|
||||
RequireCommand,
|
||||
StopCommand,
|
||||
// Action commands
|
||||
|
|
@ -128,8 +129,6 @@ export const
|
|||
ReplaceCommand,
|
||||
EncloseCommand,
|
||||
ExtractTextCommand,
|
||||
// rfc6134
|
||||
ValidExtListTest,
|
||||
// rfc6609
|
||||
IncludeCommand,
|
||||
ReturnCommand,
|
||||
|
|
@ -147,17 +146,6 @@ export const
|
|||
return commands;
|
||||
},
|
||||
|
||||
unavailableCommands = () => {
|
||||
let commands = {};
|
||||
AllCommands.forEach(cmd => {
|
||||
const obj = new cmd, requires = obj.require;
|
||||
if (requires && !(Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string))) {
|
||||
commands[obj.identifier] = cmd;
|
||||
}
|
||||
});
|
||||
return commands;
|
||||
},
|
||||
|
||||
availableActions = () => {
|
||||
let actions = {}, id;
|
||||
AllCommands.forEach(cmd => {
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ export class FileIntoCommand extends ActionCommand
|
|||
super();
|
||||
// QuotedString / MultiLine
|
||||
this._mailbox = new GrammarQuotedString();
|
||||
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||
this.copy = false;
|
||||
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
|
||||
this.create = false;
|
||||
}
|
||||
|
||||
get require() { return 'fileinto'; }
|
||||
|
|
@ -32,7 +28,9 @@ export class FileIntoCommand extends ActionCommand
|
|||
toString()
|
||||
{
|
||||
return 'fileinto'
|
||||
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
|
||||
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
|
||||
+ ((this.create && capa.includes('mailbox')) ? ' :create' : '')
|
||||
+ ' ' + this._mailbox
|
||||
+ ';';
|
||||
|
|
@ -66,17 +64,15 @@ export class RedirectCommand extends ActionCommand
|
|||
super();
|
||||
// QuotedString / MultiLine
|
||||
this._address = new GrammarQuotedString();
|
||||
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||
this.copy = false;
|
||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
||||
this.list = null;
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
|
||||
return 'redirect'
|
||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
||||
// + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '')
|
||||
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
|
||||
+ ' ' + this._address
|
||||
+ ';';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import {
|
||||
ControlCommand,
|
||||
GrammarCommands,
|
||||
GrammarStringList,
|
||||
GrammarQuotedString
|
||||
} from 'Sieve/Grammar';
|
||||
|
|
@ -17,55 +16,37 @@ import {
|
|||
* elsif <test2: test> <block2: block>
|
||||
* else <block3: block>
|
||||
*/
|
||||
export /*abstract*/ class ConditionalCommand extends ControlCommand
|
||||
export class ConditionalCommand extends ControlCommand
|
||||
{
|
||||
constructor()
|
||||
constructor(identifier)
|
||||
{
|
||||
/*
|
||||
if (this.constructor == ConditionalCommand) {
|
||||
throw Error("Abstract class can't be instantiated.");
|
||||
}
|
||||
*/
|
||||
super();
|
||||
this.commands = new GrammarCommands;
|
||||
}
|
||||
}
|
||||
|
||||
export class IfCommand extends ConditionalCommand
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this._test = null; // must be descendent instanceof TestCommand
|
||||
}
|
||||
|
||||
get test()
|
||||
{
|
||||
return this._test;
|
||||
}
|
||||
|
||||
set test(value)
|
||||
{
|
||||
/*
|
||||
if (!value instanceof TestCommand) {
|
||||
throw Error("test must be descendent instanceof TestCommand.");
|
||||
}
|
||||
*/
|
||||
this._test = value;
|
||||
super(identifier);
|
||||
this.test = null;
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
/*
|
||||
if (!this._test instanceof TestCommand) {
|
||||
throw Error("test must be descendent instanceof TestCommand.");
|
||||
}
|
||||
*/
|
||||
return this.identifier + ' ' + this._test + ' ' + this.commands;
|
||||
return this.identifier + ' ' + this.test + ' ' + this.commands;
|
||||
}
|
||||
/*
|
||||
public function pushArguments(array $args): void
|
||||
{
|
||||
args.forEach((arg, i) => {
|
||||
if (i && ':' === args[i-1][0]) {
|
||||
this[args[i-1].replace(':','_')].value = arg.value;
|
||||
}
|
||||
});
|
||||
print_r($args);
|
||||
exit;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
export class ElsIfCommand extends IfCommand
|
||||
export class IfCommand extends ConditionalCommand
|
||||
{
|
||||
}
|
||||
|
||||
export class ElsIfCommand extends ConditionalCommand
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export class AddressTest extends TestCommand
|
|||
this.header_list = new GrammarStringList;
|
||||
this.key_list = new GrammarStringList;
|
||||
// rfc5260#section-6
|
||||
this.index = new GrammarNumber('');
|
||||
this.last = false;
|
||||
// this.index = new GrammarNumber;
|
||||
// this.last = false;
|
||||
// rfc5703#section-6
|
||||
// this.mime
|
||||
// this.anychild
|
||||
|
|
@ -67,11 +67,10 @@ export class AddressTest extends TestCommand
|
|||
}
|
||||
}
|
||||
return result
|
||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.address_part
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.header_list
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
|
@ -148,10 +147,9 @@ export class EnvelopeTest extends TestCommand
|
|||
toString()
|
||||
{
|
||||
return 'envelope'
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.address_part
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.envelope_part
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
|
@ -236,8 +234,8 @@ export class HeaderTest extends TestCommand
|
|||
this.header_names = new GrammarStringList;
|
||||
this.key_list = new GrammarStringList;
|
||||
// rfc5260#section-6
|
||||
this.index = new GrammarNumber('');
|
||||
this.last = false;
|
||||
// this.index = new GrammarNumber;
|
||||
// this.last = false;
|
||||
// rfc5703#section-6
|
||||
this.mime = false;
|
||||
this.anychild = false;
|
||||
|
|
@ -280,10 +278,9 @@ export class HeaderTest extends TestCommand
|
|||
}
|
||||
}
|
||||
return result
|
||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.header_names
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
|
@ -319,6 +316,11 @@ export class NotTest extends TestCommand
|
|||
{
|
||||
return 'not ' + this.test;
|
||||
}
|
||||
|
||||
pushArguments()
|
||||
{
|
||||
throw 'No arguments';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,9 +22,8 @@ export class BodyTest extends TestCommand
|
|||
toString()
|
||||
{
|
||||
return 'body'
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.body_transform
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,28 +13,24 @@ export class EnvironmentTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._name = new GrammarQuotedString;
|
||||
this.name = new GrammarQuotedString;
|
||||
this.key_list = new GrammarStringList;
|
||||
}
|
||||
|
||||
get name() { return this._name.value; }
|
||||
set name(v) { this._name.value = v; }
|
||||
|
||||
get require() { return 'environment'; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'environment'
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this._name
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.name
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.key_list = args.pop();
|
||||
this._name = args.pop();
|
||||
this.name = args.pop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,6 @@ export class SetCommand extends ActionCommand
|
|||
|
||||
get require() { return 'variables'; }
|
||||
|
||||
get name() { return this._name.value; }
|
||||
set name(str) { this._name.value = str; }
|
||||
|
||||
get value() { return this._value.value; }
|
||||
set value(str) { this._value.value = str; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'set'
|
||||
|
|
@ -35,6 +29,12 @@ export class SetCommand extends ActionCommand
|
|||
+ ' ' + this._value;
|
||||
}
|
||||
|
||||
get name() { return this._name.value; }
|
||||
set name(str) { this._name.value = str; }
|
||||
|
||||
get value() { return this._value.value; }
|
||||
set value(str) { this._value.value = str; }
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
||||
|
|
@ -57,9 +57,8 @@ export class StringTest extends TestCommand
|
|||
toString()
|
||||
{
|
||||
return 'string'
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.source
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,33 @@ export class VacationCommand extends ActionCommand
|
|||
// get require() { return ['vacation','vacation-seconds']; }
|
||||
get require() { return 'vacation'; }
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = 'vacation';
|
||||
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
|
||||
result += ' :seconds ' + this._seconds;
|
||||
} else if (0 < this._days.value) {
|
||||
result += ' :days ' + this._days;
|
||||
}
|
||||
if (this._subject.length) {
|
||||
result += ' :subject ' + this._subject;
|
||||
}
|
||||
if (this._from.length) {
|
||||
result += ' :from ' + this._from;
|
||||
// result += ' :from ' + this.arguments[':from'];
|
||||
}
|
||||
if (this.addresses.length) {
|
||||
result += ' :addresses ' + this.addresses;
|
||||
}
|
||||
if (this.mime) {
|
||||
result += ' :mime';
|
||||
}
|
||||
if (this._handle.length) {
|
||||
result += ' :handle ' + this._handle;
|
||||
}
|
||||
return result + ' ' + this._reason;
|
||||
}
|
||||
|
||||
get days() { return this._days.value; }
|
||||
get seconds() { return this._seconds.value; }
|
||||
get subject() { return this._subject.value; }
|
||||
|
|
@ -44,32 +71,6 @@ export class VacationCommand extends ActionCommand
|
|||
set handle(str) { this._handle.value = str; }
|
||||
set reason(str) { this._reason.value = str; }
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = 'vacation';
|
||||
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
|
||||
result += ' :seconds ' + this._seconds;
|
||||
} else if (0 < this._days.value) {
|
||||
result += ' :days ' + this._days;
|
||||
}
|
||||
if (this._subject.length) {
|
||||
result += ' :subject ' + this._subject;
|
||||
}
|
||||
if (this._from.length) {
|
||||
result += ' :from ' + this._from;
|
||||
}
|
||||
if (this.addresses.length) {
|
||||
result += ' :addresses ' + this.addresses;
|
||||
}
|
||||
if (this.mime) {
|
||||
result += ' :mime';
|
||||
}
|
||||
if (this._handle.length) {
|
||||
result += ' :handle ' + this._handle;
|
||||
}
|
||||
return result + ' ' + this._reason;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this._reason.value = args.pop().value; // GrammarQuotedString
|
||||
|
|
@ -80,8 +81,7 @@ export class VacationCommand extends ActionCommand
|
|||
this.addresses = arg; // GrammarStringList
|
||||
} else if (i && ':' === args[i-1][0]) {
|
||||
// :days, :seconds, :subject, :from, :handle
|
||||
let p = args[i-1].replace(':','_');
|
||||
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||
this[args[i-1].replace(':','_')].value = arg.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,12 +43,11 @@ class FlagCommand extends ActionCommand
|
|||
if (args[0] instanceof GrammarQuotedString) {
|
||||
this._variablename = args[0];
|
||||
}
|
||||
args[0] = args[1];
|
||||
}
|
||||
if (args[0] instanceof GrammarStringList) {
|
||||
if (args[1] instanceof GrammarString) {
|
||||
this.list_of_flags = args[1];
|
||||
}
|
||||
} else if (args[0] instanceof GrammarString) {
|
||||
this.list_of_flags = args[0];
|
||||
} else if (args[0]) {
|
||||
this.list_of_flags.push(args[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,9 +78,8 @@ export class HasFlagTest extends TestCommand
|
|||
toString()
|
||||
{
|
||||
return 'hasflag'
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.variable_list
|
||||
+ ' ' + this.list_of_flags;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import {
|
||||
GrammarQuotedString,
|
||||
GrammarString,
|
||||
TestCommand
|
||||
} from 'Sieve/Grammar';
|
||||
|
||||
|
|
@ -13,23 +14,19 @@ export class SpamTestTest extends TestCommand
|
|||
{
|
||||
super();
|
||||
this.percent = false, // 0 - 100 else 0 - 10
|
||||
this._value = new GrammarQuotedString;
|
||||
this.value = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
|
||||
get require() { return /:value|:count/.test(this._match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
|
||||
|
||||
get value() { return this._value.value; }
|
||||
set value(v) { this._value.value = v; }
|
||||
get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'spamtest'
|
||||
+ (this.percent ? ' :percent' : '')
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this._value;
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.value;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
|
|
@ -37,8 +34,8 @@ export class SpamTestTest extends TestCommand
|
|||
args.forEach(arg => {
|
||||
if (':percent' === arg) {
|
||||
this.percent = true;
|
||||
} else if (arg instanceof GrammarQuotedString) {
|
||||
this._value = arg;
|
||||
} else if (arg instanceof GrammarString) {
|
||||
this.value = arg;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -49,28 +46,24 @@ export class VirusTestTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._value = new GrammarQuotedString; // 1 - 5
|
||||
this.value = new GrammarQuotedString; // 1 - 5
|
||||
}
|
||||
|
||||
get require() { return ':value' == this._match_type ? ['virustest','relational'] : 'virustest'; }
|
||||
|
||||
get value() { return this._value.value; }
|
||||
set value(v) { this._value.value = v; }
|
||||
get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'virustest'
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this._value;
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.value;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
args.forEach(arg => {
|
||||
if (arg instanceof GrammarQuotedString) {
|
||||
this._value = arg;
|
||||
if (arg instanceof GrammarString) {
|
||||
this.value = arg;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ export class DateTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._zone = new GrammarQuotedString;
|
||||
this.zone = new GrammarQuotedString;
|
||||
this.originalzone = false;
|
||||
this._header_name = new GrammarQuotedString;
|
||||
this._date_part = new GrammarQuotedString;
|
||||
this.header_name = new GrammarQuotedString;
|
||||
this.date_part = new GrammarQuotedString;
|
||||
this.key_list = new GrammarStringList;
|
||||
// rfc5260#section-6
|
||||
this.index = new GrammarNumber;
|
||||
|
|
@ -27,40 +27,30 @@ export class DateTest extends TestCommand
|
|||
// get require() { return ['date','index']; }
|
||||
get require() { return 'date'; }
|
||||
|
||||
get zone() { return this._zone.value; }
|
||||
set zone(v) { this._zone.value = v; }
|
||||
|
||||
get header_name() { return this._header_name.value; }
|
||||
set header_name(v) { this._header_name.value = v; }
|
||||
|
||||
get date_part() { return this._date_part.value; }
|
||||
set date_part(v) { this._date_part.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'date'
|
||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||
+ (this.originalzone ? ' :originalzone' : (this._zone.length ? ' :zone ' + this._zone : ''))
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this._header_name
|
||||
+ ' ' + this._date_part
|
||||
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.header_name
|
||||
+ ' ' + this.date_part
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.key_list = args.pop();
|
||||
this._date_part = args.pop();
|
||||
this._header_name = args.pop();
|
||||
this.date_part = args.pop();
|
||||
this.header_name = args.pop();
|
||||
args.forEach((arg, i) => {
|
||||
if (':originalzone' === arg) {
|
||||
this.originalzone = true;
|
||||
} else if (':last' === arg) {
|
||||
this.last = true;
|
||||
} else if (i && ':zone' === args[i-1]) {
|
||||
this._zone.value = arg.value;
|
||||
this.zone.value = arg.value;
|
||||
} else if (i && ':index' === args[i-1]) {
|
||||
this.index.value = arg.value;
|
||||
}
|
||||
|
|
@ -73,37 +63,30 @@ export class CurrentDateTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._zone = new GrammarQuotedString;
|
||||
this._date_part = new GrammarQuotedString;
|
||||
this.zone = new GrammarQuotedString;
|
||||
this.date_part = new GrammarQuotedString;
|
||||
this.key_list = new GrammarStringList;
|
||||
}
|
||||
|
||||
get require() { return 'date'; }
|
||||
|
||||
get zone() { return this._zone.value; }
|
||||
set zone(v) { this._zone.value = v; }
|
||||
|
||||
get date_part() { return this._date_part.value; }
|
||||
set date_part(v) { this._date_part.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'currentdate'
|
||||
+ (this._zone.length ? ' :zone ' + this._zone : '')
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ ' ' + this._date_part
|
||||
+ (this.zone.length ? ' :zone ' + this.zone : '')
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this.date_part
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.key_list = args.pop();
|
||||
this._date_part = args.pop();
|
||||
this.date_part = args.pop();
|
||||
args.forEach((arg, i) => {
|
||||
if (i && ':zone' === args[i-1]) {
|
||||
this._zone.value = arg.value;
|
||||
this.zone.value = arg.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,30 +16,24 @@ export class AddHeaderCommand extends ActionCommand
|
|||
{
|
||||
super();
|
||||
this.last = false;
|
||||
this._field_name = new GrammarQuotedString;
|
||||
this._value = new GrammarQuotedString;
|
||||
this.field_name = new GrammarQuotedString;
|
||||
this.value = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
get require() { return 'editheader'; }
|
||||
|
||||
get field_name() { return this._field_name.value; }
|
||||
set field_name(v) { this._field_name.value = v; }
|
||||
|
||||
get value() { return this._value.value; }
|
||||
set value(v) { this._value.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return this.identifier
|
||||
+ (this.last ? ' :last' : '')
|
||||
+ ' ' + this._field_name
|
||||
+ ' ' + this._value + ';';
|
||||
+ ' ' + this.field_name
|
||||
+ ' ' + this.value + ';';
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this._value = args.pop();
|
||||
this._field_name = args.pop();
|
||||
this.value = args.pop();
|
||||
this.field_name = args.pop();
|
||||
this.last = args.includes(':last');
|
||||
}
|
||||
}
|
||||
|
|
@ -53,22 +47,19 @@ export class DeleteHeaderCommand extends ActionCommand
|
|||
this.last = false;
|
||||
this.comparator = '',
|
||||
this.match_type = ':is',
|
||||
this._field_name = new GrammarQuotedString;
|
||||
this.field_name = new GrammarQuotedString;
|
||||
this.value_patterns = new GrammarStringList;
|
||||
}
|
||||
|
||||
get require() { return 'editheader'; }
|
||||
|
||||
get field_name() { return this._field_name.value; }
|
||||
set field_name(v) { this._field_name.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return this.identifier
|
||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.match_type
|
||||
+ ' ' + this._field_name
|
||||
+ ' ' + this.field_name
|
||||
+ ' ' + this.value_patterns + ';';
|
||||
}
|
||||
|
||||
|
|
@ -85,10 +76,10 @@ export class DeleteHeaderCommand extends ActionCommand
|
|||
});
|
||||
|
||||
if (l && args[l-1] instanceof GrammarString) {
|
||||
this._field_name = args[l-1];
|
||||
this.field_name = args[l-1];
|
||||
this.value_patterns = args[l];
|
||||
} else {
|
||||
this._field_name = args[l];
|
||||
this.field_name = args[l];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import {
|
|||
GrammarString
|
||||
} from 'Sieve/Grammar';
|
||||
|
||||
class /*abstract*/ rfc5429Command extends ActionCommand
|
||||
class rfc5429Command extends ActionCommand
|
||||
{
|
||||
constructor()
|
||||
constructor(identifier)
|
||||
{
|
||||
super();
|
||||
super(identifier);
|
||||
this._reason = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +44,7 @@ class /*abstract*/ rfc5429Command extends ActionCommand
|
|||
*/
|
||||
export class ErejectCommand extends rfc5429Command
|
||||
{
|
||||
constructor() { super('ereject'); }
|
||||
get require() { return 'ereject'; }
|
||||
}
|
||||
|
||||
|
|
@ -52,5 +53,6 @@ export class ErejectCommand extends rfc5429Command
|
|||
*/
|
||||
export class RejectCommand extends rfc5429Command
|
||||
{
|
||||
constructor() { super('reject'); }
|
||||
get require() { return 'reject'; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,16 +26,14 @@ export class NotifyCommand extends ActionCommand
|
|||
}
|
||||
|
||||
get method() { return this._method.value; }
|
||||
set method(str) { this._method.value = str; }
|
||||
|
||||
get from() { return this._from.value; }
|
||||
set from(str) { this._from.value = str; }
|
||||
|
||||
get importance() { return this._importance.value; }
|
||||
set importance(int) { this._importance.value = int; }
|
||||
|
||||
get message() { return this._message.value; }
|
||||
set message(str) { this._message.value = str; }
|
||||
|
||||
set method(str) { this._method.value = str; }
|
||||
set from(str) { this._from.value = str; }
|
||||
set importance(int) { this._importance.value = int; }
|
||||
set message(str) { this._message.value = str; }
|
||||
|
||||
get require() { return 'enotify'; }
|
||||
|
||||
|
|
@ -65,8 +63,7 @@ export class NotifyCommand extends ActionCommand
|
|||
this.options = arg; // GrammarStringList
|
||||
} else if (i && ':' === args[i-1][0]) {
|
||||
// :from, :importance, :message
|
||||
let p = args[i-1].replace(':','_');
|
||||
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||
this[args[i-1].replace(':','_')].value = arg.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -102,32 +99,25 @@ export class NotifyMethodCapabilityTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._notification_uri = new GrammarQuotedString;
|
||||
this._notification_capability = new GrammarQuotedString;
|
||||
this.notification_uri = new GrammarQuotedString;
|
||||
this.notification_capability = new GrammarQuotedString;
|
||||
this.key_list = new GrammarStringList;
|
||||
}
|
||||
|
||||
get notification_uri() { return this._notification_uri.value; }
|
||||
set notification_uri(v) { this._notification_uri.value = v; }
|
||||
|
||||
get notification_capability() { return this._notification_capability.value; }
|
||||
set notification_capability(v) { this._notification_capability.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'valid_notify_method '
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ this._notification_uri
|
||||
+ this._notification_capability
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ (this.match_type ? ' ' + this.match_type : '')
|
||||
+ this.notification_uri
|
||||
+ this.notification_capability
|
||||
+ this.key_list;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.key_list = args.pop();
|
||||
this._notification_capability = args.pop();
|
||||
this._notification_uri = args.pop();
|
||||
this.notification_capability = args.pop();
|
||||
this.notification_uri = args.pop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,21 +41,18 @@ export class ErrorCommand extends ControlCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._message = new GrammarQuotedString;
|
||||
this.message = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
get require() { return 'ihave'; }
|
||||
|
||||
get message() { return this._message.value; }
|
||||
set message(v) { this._message.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'error ' + this._message + ';';
|
||||
return 'error ' + this.message + ';';
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this._message = args.pop();
|
||||
this.message = args.pop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,35 +42,28 @@ export class MetadataTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._mailbox = new GrammarQuotedString;
|
||||
this._annotation_name = new GrammarQuotedString;
|
||||
this.mailbox = new GrammarQuotedString;
|
||||
this.annotation_name = new GrammarQuotedString;
|
||||
this.key_list = new GrammarStringList;
|
||||
}
|
||||
|
||||
get require() { return 'mboxmetadata'; }
|
||||
|
||||
get mailbox() { return this._mailbox.value; }
|
||||
set mailbox(v) { this._mailbox.value = v; }
|
||||
|
||||
get annotation_name() { return this._annotation_name.value; }
|
||||
set annotation_name(v) { this._annotation_name.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'metadata '
|
||||
+ ' ' + this._match_type
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ ' ' + this._mailbox
|
||||
+ ' ' + this._annotation_name
|
||||
+ ' ' + this.match_type
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.mailbox
|
||||
+ ' ' + this.annotation_name
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.key_list = args.pop();
|
||||
this._annotation_name = args.pop();
|
||||
this._mailbox = args.pop();
|
||||
this.annotation_name = args.pop();
|
||||
this.mailbox = args.pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,26 +75,23 @@ export class MetadataExistsTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._mailbox = new GrammarQuotedString;
|
||||
this.mailbox = new GrammarQuotedString;
|
||||
this.annotation_names = new GrammarStringList;
|
||||
}
|
||||
|
||||
get require() { return 'mboxmetadata'; }
|
||||
|
||||
get mailbox() { return this._mailbox.value; }
|
||||
set mailbox(v) { this._mailbox.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'metadataexists '
|
||||
+ ' ' + this._mailbox
|
||||
+ ' ' + this.mailbox
|
||||
+ ' ' + this.annotation_names;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.annotation_names = args.pop();
|
||||
this._mailbox = args.pop();
|
||||
this.mailbox = args.pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,29 +103,25 @@ export class ServerMetadataTest extends TestCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this._annotation_name = new GrammarQuotedString;
|
||||
this.annotation_name = new GrammarQuotedString;
|
||||
this.key_list = new GrammarStringList;
|
||||
}
|
||||
|
||||
get require() { return 'servermetadata'; }
|
||||
|
||||
get annotation_name() { return this._annotation_name.value; }
|
||||
set annotation_name(v) { this._annotation_name.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'servermetadata '
|
||||
+ ' ' + this._match_type
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ ' ' + this._annotation_name
|
||||
+ ' ' + this.match_type
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ ' ' + this.annotation_name
|
||||
+ ' ' + this.key_list;
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this.key_list = args.pop();
|
||||
this._annotation_name = args.pop();
|
||||
this.annotation_name = args.pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
import {
|
||||
ActionCommand,
|
||||
ControlCommand,
|
||||
GrammarCommands,
|
||||
GrammarNumber,
|
||||
GrammarQuotedString,
|
||||
GrammarString,
|
||||
|
|
@ -20,8 +19,7 @@ export class ForEveryPartCommand extends ControlCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this.name = new GrammarString;
|
||||
this.commands = new GrammarCommands;
|
||||
this._name = new GrammarString;
|
||||
}
|
||||
|
||||
get require() { return 'foreverypart'; }
|
||||
|
|
@ -29,8 +27,8 @@ export class ForEveryPartCommand extends ControlCommand
|
|||
toString()
|
||||
{
|
||||
let result = 'foreverypart';
|
||||
if (this.name.length) {
|
||||
result += ' :name ' + this.name;
|
||||
if (this._subject.length) {
|
||||
result += ' :name ' + this._name;
|
||||
}
|
||||
return result + ' ' + this.commands;
|
||||
}
|
||||
|
|
@ -39,22 +37,19 @@ export class ForEveryPartCommand extends ControlCommand
|
|||
{
|
||||
args.forEach((arg, i) => {
|
||||
if (':name' === arg) {
|
||||
this.name.value = args[i+1].value;
|
||||
this._name.value = args[i+1].value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be inside foreverypart
|
||||
*/
|
||||
export class BreakCommand extends ForEveryPartCommand
|
||||
{
|
||||
toString()
|
||||
{
|
||||
let result = 'break';
|
||||
if (this.name.length) {
|
||||
result += ' :name ' + this.name;
|
||||
if (this._subject.length) {
|
||||
result += ' :name ' + this._name;
|
||||
}
|
||||
return result + ';';
|
||||
}
|
||||
|
|
@ -68,23 +63,14 @@ export class ReplaceCommand extends ActionCommand
|
|||
constructor()
|
||||
{
|
||||
super();
|
||||
this.mime = false;
|
||||
this._subject = new GrammarQuotedString;
|
||||
this._from = new GrammarQuotedString;
|
||||
this._replacement = new GrammarQuotedString;
|
||||
this.mime = false;
|
||||
this._subject = new GrammarQuotedString;
|
||||
this._from = new GrammarQuotedString;
|
||||
this.replacement = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
get require() { return 'replace'; }
|
||||
|
||||
get subject() { return this._subject.value; }
|
||||
set subject(str) { this._subject.value = str; }
|
||||
|
||||
get from() { return this._from.value; }
|
||||
set from(str) { this._from.value = str; }
|
||||
|
||||
get replacement() { return this._replacement.value; }
|
||||
set replacement(str) { this._replacement.value = str; }
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = 'replace';
|
||||
|
|
@ -96,20 +82,20 @@ export class ReplaceCommand extends ActionCommand
|
|||
}
|
||||
if (this._from.length) {
|
||||
result += ' :from ' + this._from;
|
||||
// result += ' :from ' + this.arguments[':from'];
|
||||
}
|
||||
return result + this._replacement + ';';
|
||||
return result + this.replacement + ';';
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this._replacement = args.pop();
|
||||
this.replacement = args.pop();
|
||||
args.forEach((arg, i) => {
|
||||
if (':mime' === arg) {
|
||||
this.mime = true;
|
||||
} else if (i && ':' === args[i-1][0]) {
|
||||
// :subject, :from
|
||||
let p = args[i-1].replace(':','_');
|
||||
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||
this[args[i-1].replace(':','_')].value = arg.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -129,9 +115,6 @@ export class EncloseCommand extends ActionCommand
|
|||
|
||||
get require() { return 'enclose'; }
|
||||
|
||||
get subject() { return this._subject.value; }
|
||||
set subject(v) { this._subject.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = 'enclose';
|
||||
|
|
@ -149,8 +132,7 @@ export class EncloseCommand extends ActionCommand
|
|||
args.forEach((arg, i) => {
|
||||
if (i && ':' === args[i-1][0]) {
|
||||
// :subject, :headers
|
||||
let p = args[i-1].replace(':','_');
|
||||
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||
this[args[i-1].replace(':','_')].value = arg.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -158,7 +140,6 @@ export class EncloseCommand extends ActionCommand
|
|||
|
||||
/**
|
||||
* https://datatracker.ietf.org/doc/html/rfc5703#section-7
|
||||
* Should be inside foreverypart, else empty and flagged as a compilation error
|
||||
*/
|
||||
export class ExtractTextCommand extends ActionCommand
|
||||
{
|
||||
|
|
@ -167,12 +148,9 @@ export class ExtractTextCommand extends ActionCommand
|
|||
super();
|
||||
this.modifiers = [];
|
||||
this._first = new GrammarNumber;
|
||||
this._varname = new GrammarQuotedString;
|
||||
this.varname = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
get varname() { return this._varname.value; }
|
||||
set varname(v) { this._varname.value = v; }
|
||||
|
||||
get require() { return 'extracttext'; }
|
||||
|
||||
toString()
|
||||
|
|
@ -182,12 +160,12 @@ export class ExtractTextCommand extends ActionCommand
|
|||
if (0 < this._first.value) {
|
||||
result += ' :first ' + this._first;
|
||||
}
|
||||
return result + ' ' + this._varname + ';';
|
||||
return result + ' ' + this.varname + ';';
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
{
|
||||
this._varname = args.pop();
|
||||
this.varname = args.pop();
|
||||
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
||||
args.includes(modifier) && this.modifiers.push(modifier);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class ValidExtListTest extends TestCommand
|
|||
{
|
||||
constructor()
|
||||
{
|
||||
super('valid_ext_list');
|
||||
super();
|
||||
this.ext_list_names = new GrammarStringList;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,21 +16,18 @@ export class IncludeCommand extends ControlCommand
|
|||
this.global = false; // ':personal' / ':global';
|
||||
this.once = false;
|
||||
this.optional = false;
|
||||
this._value = new GrammarQuotedString;
|
||||
this.value = new GrammarQuotedString;
|
||||
}
|
||||
|
||||
get require() { return 'include'; }
|
||||
|
||||
get value() { return this._value.value; }
|
||||
set value(v) { this._value.value = v; }
|
||||
|
||||
toString()
|
||||
{
|
||||
return 'include'
|
||||
+ (this.global ? ' :global' : '')
|
||||
+ (this.once ? ' :once' : '')
|
||||
+ (this.optional ? ' :optional' : '')
|
||||
+ ' ' + this._value + ';';
|
||||
+ ' ' + this.value + ';';
|
||||
}
|
||||
|
||||
pushArguments(args)
|
||||
|
|
@ -39,7 +36,7 @@ export class IncludeCommand extends ControlCommand
|
|||
if (':global' === arg || ':once' === arg || ':optional' === arg) {
|
||||
this[arg.slice(1)] = true;
|
||||
} else if (arg instanceof GrammarQuotedString) {
|
||||
this._value = arg;
|
||||
this.value = arg;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
MULTILINE_DOTSTART
|
||||
} from 'Sieve/RegEx';
|
||||
|
||||
import { arrayToString, getMatchTypes, getComparators } from 'Sieve/Utils';
|
||||
import { arrayToString, getMatchTypes } from 'Sieve/Utils';
|
||||
|
||||
/**
|
||||
* abstract
|
||||
|
|
@ -18,7 +18,7 @@ export class GrammarString /*extends String*/
|
|||
{
|
||||
constructor(value = '')
|
||||
{
|
||||
this._value = value.toString ? value.toString() : value;
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
toString()
|
||||
|
|
@ -45,41 +45,41 @@ export class GrammarString /*extends String*/
|
|||
/**
|
||||
* abstract
|
||||
*/
|
||||
export /*abstract*/ class GrammarComment extends GrammarString
|
||||
export 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
|
||||
*/
|
||||
const cmdNameSuffix = /(test|command|action)$/;
|
||||
export /*abstract*/ class GrammarCommand
|
||||
export class GrammarCommand
|
||||
{
|
||||
constructor(identifier)
|
||||
{
|
||||
/*
|
||||
if (this.constructor == GrammarCommand) {
|
||||
throw Error("Abstract class can't be instantiated.");
|
||||
}
|
||||
*/
|
||||
this.identifier = identifier || this.constructor.name.toLowerCase().replace(cmdNameSuffix, '');
|
||||
this.identifier = identifier || this.constructor.name.toLowerCase().replace(/(test|command|action)$/, '');
|
||||
this.arguments = [];
|
||||
this.commands = new GrammarCommands;
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = this.identifier;
|
||||
if (this.arguments?.length) {
|
||||
if (this.arguments.length) {
|
||||
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)
|
||||
|
|
@ -108,107 +108,78 @@ export class GrammarCommands extends Array
|
|||
/**
|
||||
* https://tools.ietf.org/html/rfc5228#section-3
|
||||
*/
|
||||
export /*abstract*/ class ControlCommand extends GrammarCommand
|
||||
export class ControlCommand extends GrammarCommand
|
||||
{
|
||||
/*
|
||||
constructor(identifier)
|
||||
{
|
||||
if (this.constructor == ControlCommand) {
|
||||
throw Error("Abstract class can't be instantiated.");
|
||||
}
|
||||
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 : ';'
|
||||
);
|
||||
}
|
||||
|
||||
getComparators()
|
||||
{
|
||||
return ['i;ascii-casemap'];
|
||||
}
|
||||
|
||||
getMatchTypes()
|
||||
{
|
||||
return [':is', ':contains', ':matches'];
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc5228#section-4
|
||||
*/
|
||||
export /*abstract*/ class ActionCommand extends GrammarCommand
|
||||
export class ActionCommand extends GrammarCommand
|
||||
{
|
||||
/*
|
||||
constructor(identifier)
|
||||
{
|
||||
if (this.constructor == ActionCommand) {
|
||||
throw Error("Abstract class can't be instantiated.");
|
||||
}
|
||||
super(identifier);
|
||||
}
|
||||
*/
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = this.identifier;
|
||||
if (this.arguments.length) {
|
||||
result += ' ' + arrayToString(this.arguments, ' ');
|
||||
}
|
||||
return result + ';'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc5228#section-5
|
||||
*/
|
||||
export /*abstract*/ class TestCommand extends GrammarCommand
|
||||
export class TestCommand extends GrammarCommand
|
||||
{
|
||||
constructor(identifier)
|
||||
{
|
||||
/*
|
||||
if (this.constructor == TestCommand) {
|
||||
throw Error("Abstract class can't be instantiated.");
|
||||
}
|
||||
*/
|
||||
super(identifier);
|
||||
// Almost every test has a comparator and match_type, so define them here
|
||||
this._comparator = '';
|
||||
this._match_type = '';
|
||||
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 = '';
|
||||
}
|
||||
this.comparator = '';
|
||||
this.match_type = ':is';
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
||||
if (!getMatchTypes().includes(this.match_type)) {
|
||||
throw 'Unsupported match-type ' + this.match_type;
|
||||
}
|
||||
return (this.identifier
|
||||
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||
+ (this._match_type ? ' ' + this._match_type : '')
|
||||
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||
+ (this.match_type ? ' ' + this.match_type : '')
|
||||
+ ' ' + arrayToString(this.arguments, ' ')).trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -280,7 +251,6 @@ export class GrammarStringList extends Array
|
|||
{
|
||||
toString()
|
||||
{
|
||||
// if there is only a single string, the brackets are optional
|
||||
if (1 < this.length) {
|
||||
return '[' + this.join(',') + ']';
|
||||
}
|
||||
|
|
@ -289,7 +259,7 @@ export class GrammarStringList extends Array
|
|||
|
||||
push(value)
|
||||
{
|
||||
if (!(value instanceof GrammarQuotedString)) {
|
||||
if (!(value instanceof GrammarString)) {
|
||||
value = new GrammarQuotedString(value);
|
||||
}
|
||||
super.push(value);
|
||||
|
|
@ -316,14 +286,10 @@ GrammarStringList.fromString = list => {
|
|||
|
||||
export class GrammarQuotedString extends GrammarString
|
||||
{
|
||||
constructor(value = '')
|
||||
{
|
||||
super(value instanceof GrammarQuotedString ? value.value : value);
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
|
||||
// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,23 +324,3 @@ GrammarMultiLine.fromString = string => {
|
|||
}
|
||||
return new GrammarMultiLine();
|
||||
}
|
||||
|
||||
export class UnknownCommand extends GrammarCommand
|
||||
{
|
||||
constructor(identifier)
|
||||
{
|
||||
super(identifier);
|
||||
this.commands = new GrammarCommands;
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
let result = this.identifier;
|
||||
if (this.arguments?.length) {
|
||||
result += ' ' + arrayToString(this.arguments, ' ');
|
||||
}
|
||||
return result + (
|
||||
this.commands?.length ? ' ' + this.commands : ';'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
import {
|
||||
GrammarBracketComment,
|
||||
GrammarCommand,
|
||||
GrammarComment,
|
||||
GrammarHashComment,
|
||||
GrammarMultiLine,
|
||||
GrammarNumber,
|
||||
|
|
@ -28,8 +27,8 @@ import {
|
|||
GrammarTestList
|
||||
} from 'Sieve/Grammar';
|
||||
|
||||
import { availableCommands, unavailableCommands } from 'Sieve/Commands';
|
||||
import { ConditionalCommand, IfCommand, RequireCommand } from 'Sieve/Commands/Controls';
|
||||
import { availableCommands } from 'Sieve/Commands';
|
||||
import { ConditionalCommand, RequireCommand } from 'Sieve/Commands/Controls';
|
||||
import { NotTest } from 'Sieve/Commands/Tests';
|
||||
|
||||
const
|
||||
|
|
@ -93,7 +92,6 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
|
||||
// Only activate available commands
|
||||
const Commands = availableCommands();
|
||||
const disabledCommands = unavailableCommands();
|
||||
|
||||
let match,
|
||||
line = 1,
|
||||
|
|
@ -119,20 +117,18 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
let prev_arg = args[args.length-1];
|
||||
if (getMatchTypes(0).includes(arg)) {
|
||||
command.match_type = arg;
|
||||
} else if (getMatchTypes(0).includes(prev_arg)) {
|
||||
} else if (':value' === prev_arg || ':count' === prev_arg) {
|
||||
// Sieve relational [RFC5231] match types
|
||||
/^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
|
||||
command.match_type = prev_arg + ' ' + arg;
|
||||
--args.length;
|
||||
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;
|
||||
}
|
||||
// requires.push('relational');
|
||||
} else if (':comparator' === prev_arg) {
|
||||
command.comparator = arg;
|
||||
--args.length;
|
||||
} else {
|
||||
args.push(arg);
|
||||
}
|
||||
args.push(arg);
|
||||
},
|
||||
pushArgs = () => {
|
||||
if (args.length) {
|
||||
|
|
@ -158,30 +154,27 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
pushArgs();
|
||||
value = value.toLowerCase();
|
||||
let new_command;
|
||||
if (Commands[value]) {
|
||||
if ('elsif' === value || 'else' === value) {
|
||||
let valid = false, cmd = (command ? command?.commands : tree), i = cmd?.length;
|
||||
while (i) {
|
||||
cmd[--i];
|
||||
if (cmd[i] instanceof IfCommand) {
|
||||
valid = true;
|
||||
break;
|
||||
} else if (typeof cmd[i] !== 'string' && !(cmd[i] instanceof GrammarComment)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
valid || error('Not after IF/ELSIF condition');
|
||||
}
|
||||
if ('if' === value) {
|
||||
new_command = new ConditionalCommand(value);
|
||||
} else if ('elsif' === value || 'else' === value) {
|
||||
// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
|
||||
new_command = new ConditionalCommand(value);
|
||||
} else if (Commands[value]) {
|
||||
if ('allof' === value || 'anyof' === value) {
|
||||
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
|
||||
}
|
||||
new_command = new Commands[value]();
|
||||
} else if (disabledCommands[value]) {
|
||||
console.error('Unsupported command: ' + value);
|
||||
new_command = new disabledCommands[value]();
|
||||
} else {
|
||||
console.error('Unknown command: ' + value);
|
||||
new_command = new GrammarCommand(value);
|
||||
if (command && (
|
||||
command instanceof ConditionalCommand
|
||||
|| command instanceof NotTest
|
||||
|| command.tests instanceof GrammarTestList)) {
|
||||
console.error('Unknown test: ' + value);
|
||||
new_command = new TestCommand(value);
|
||||
} else {
|
||||
console.error('Unknown command: ' + value);
|
||||
new_command = new GrammarCommand(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (new_command instanceof TestCommand) {
|
||||
|
|
@ -199,7 +192,7 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
if (command.commands) {
|
||||
command.commands.push(new_command);
|
||||
} else {
|
||||
error('command "' + new_command.identifier + '" not allowed in "' + command.identifier + '" command');
|
||||
error('commands not allowed in "' + command.identifier + '" command');
|
||||
}
|
||||
} else {
|
||||
tree.push(new_command);
|
||||
|
|
@ -226,8 +219,7 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
pushArg(GrammarMultiLine.fromString(value));
|
||||
break;
|
||||
case T_QUOTED_STRING:
|
||||
try { value = JSON.parse(value); } catch(e) { console.error(e, value); }
|
||||
pushArg(new GrammarQuotedString(value));
|
||||
pushArg(new GrammarQuotedString(value.slice(1,-1)));
|
||||
break;
|
||||
case T_NUMBER:
|
||||
pushArg(new GrammarNumber(value));
|
||||
|
|
@ -276,6 +268,7 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
break;
|
||||
case T_BLOCK_END:
|
||||
(command instanceof ConditionalCommand) || error(TokenError[type]);
|
||||
// prev_command = command;
|
||||
command = levels.up();
|
||||
break;
|
||||
|
||||
|
|
@ -300,6 +293,5 @@ export const parseScript = (script, name = 'script.sieve') => {
|
|||
}
|
||||
|
||||
tree.requires = requires;
|
||||
tree.toString = () => tree.join('\r\n');
|
||||
return tree;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const
|
|||
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||
|
||||
arrayToString = (arr, separator) =>
|
||||
(arr || []).map(item => item.toString?.() || item).join(separator),
|
||||
arr.map(item => item.toString?.() || item).join(separator),
|
||||
/*
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notifications, code);
|
||||
|
|
@ -58,33 +58,11 @@ export const
|
|||
serverErrorDesc(text);
|
||||
},
|
||||
|
||||
getComparators = (validOnly = 0) => {
|
||||
let result = [
|
||||
// Default
|
||||
'i;ascii-casemap',
|
||||
];
|
||||
if (capa.includes('relational') || !validOnly) {
|
||||
result.push('i;octet');
|
||||
}
|
||||
if (capa.includes('comparator-i;ascii-numeric') || !validOnly) {
|
||||
result.push('i;ascii-numeric');
|
||||
}
|
||||
if (capa.includes('comparator-i;unicode-casemap') || !validOnly) {
|
||||
result.push('i;unicode-casemap');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
getMatchTypes = (validOnly = 1) => {
|
||||
let result = [':is',':contains',':matches'];
|
||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
||||
// Only available for tests with a key_list property
|
||||
if (capa.includes('extlists') || !validOnly) {
|
||||
result.push(':list');
|
||||
}
|
||||
if (capa.includes('relational') || !validOnly) {
|
||||
result.push(':value');
|
||||
result.push(':count');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ const
|
|||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||
|
||||
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
|
||||
|
||||
foldersWalk = folders => {
|
||||
folders.forEach(oItem => {
|
||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||
|
|
@ -37,7 +39,7 @@ const
|
|||
id: oItem.fullName,
|
||||
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
||||
system: false,
|
||||
disabled: !oItem.selectable()
|
||||
disabled: !oItem.selectable() || disabled == oItem.fullName
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +68,7 @@ export class FilterPopupView extends rl.pluginPopupView {
|
|||
});
|
||||
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
this.folderSelectList = koComputable(folderListOptionsBuilder);
|
||||
this.folderSelectList = koComputable(() => folderListOptionsBuilder());
|
||||
|
||||
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
|
|||
|
||||
if (iError) {
|
||||
self.saveError(true);
|
||||
self.errorText(data?.messageAdditional || getNotification(iError));
|
||||
self.errorText(data?.ErrorMessageAdditional || getNotification(iError));
|
||||
} else {
|
||||
script.exists() || scripts.push(script);
|
||||
script.exists(true);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ try {
|
|||
data = data ? decodeURIComponent(data[2]) : null;
|
||||
data = data ? JSON.parse(data) : {};
|
||||
win[sName] = {
|
||||
getItem: key => data[key] ?? null,
|
||||
getItem: key => data[key] == null ? null : data[key],
|
||||
setItem: (key, value) => {
|
||||
data[key] = ''+value; // forces the value to a string
|
||||
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
||||
|
|
|
|||
|
|
@ -1,155 +1,8 @@
|
|||
import { AskPopupView } from 'View/Popup/Ask';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { SettingsGet } from '../Common/Globals';
|
||||
import { isArray } from '../Common/Utils';
|
||||
|
||||
export const Passphrases = new WeakMap();
|
||||
|
||||
Passphrases.ask = async (key, sAskDesc, btnText) => {
|
||||
if (Passphrases.has(key)) {
|
||||
return { password: Passphrases.handle(key)/*, remember:false*/ };
|
||||
} else if (Passphrases.hasInLocalStorage(key)) {
|
||||
return { password: await getFromLocalStorage(key) };
|
||||
} else {
|
||||
const pass = await AskPopupView.password(sAskDesc, btnText,
|
||||
window.crypto.subtle && canUseLocalStorage(key) ? 0b1101 : 0b0101);
|
||||
pass.rememberPermanent && await saveToLocalStorage(key, pass.password);
|
||||
return pass;
|
||||
}
|
||||
};
|
||||
|
||||
Passphrases._deleteFromSession = Passphrases.delete;
|
||||
|
||||
const timeouts = {};
|
||||
// get/set accessor to control deletion after N minutes of inactivity
|
||||
Passphrases.handle = (key, pass) => {
|
||||
const timeout = SettingsUserStore.keyPassForget();
|
||||
if (timeout && !timeouts[key]) {
|
||||
timeouts[key] = (() => Passphrases._deleteFromSession(key)).debounce(timeout * 60 * 1000);
|
||||
}
|
||||
pass && Passphrases.set(key, pass);
|
||||
timeout && timeouts[key]();
|
||||
return Passphrases.get(key);
|
||||
};
|
||||
|
||||
const deleteFromLocalStorage = (key) => {
|
||||
const keyId = getKeyId(key);
|
||||
if (keyId) {
|
||||
localStorage.removeItem(keyId);
|
||||
}
|
||||
};
|
||||
Passphrases.delete = (key) => {
|
||||
deleteFromLocalStorage(key);
|
||||
return Passphrases._deleteFromSession(key);
|
||||
};
|
||||
|
||||
Passphrases.hasInLocalStorage = (key) => {
|
||||
const keyId = getKeyId(key);
|
||||
return keyId && localStorage.getItem(keyId) !== null;
|
||||
};
|
||||
|
||||
const saveToLocalStorage = async (key, pass) => {
|
||||
const keyId = getKeyId(key);
|
||||
if (!keyId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pass) {
|
||||
localStorage.removeItem(keyId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const salt = window.crypto.getRandomValues(new Uint8Array(16));
|
||||
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), salt);
|
||||
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
derivedKey,
|
||||
new TextEncoder().encode(pass)
|
||||
);
|
||||
localStorage.setItem(keyId, JSON.stringify([
|
||||
btoa(String.fromCharCode.apply(null, salt)),
|
||||
btoa(String.fromCharCode.apply(null, iv)),
|
||||
btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)))
|
||||
]));
|
||||
} catch (e) {
|
||||
console.error('Passphrases.saveToLocalStorage failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
const getFromLocalStorage = async (key) => {
|
||||
const keyId = getKeyId(key);
|
||||
if (!keyId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const jsonData = localStorage.getItem(keyId);
|
||||
if (!jsonData) {
|
||||
console.error('Passphrases.getFromLocalStorage failed: no data found');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const saltIvData = JSON.parse(jsonData);
|
||||
if (!saltIvData || !isArray(saltIvData) || saltIvData.length !== 3) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error('invalid passphrase data');
|
||||
}
|
||||
const toUint8 = (str) => new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0)));
|
||||
|
||||
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), toUint8(saltIvData[0]));
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: toUint8(saltIvData[1])
|
||||
},
|
||||
derivedKey,
|
||||
toUint8(saltIvData[2])
|
||||
);
|
||||
return String.fromCharCode.apply(null, new Uint8Array(decrypted));
|
||||
} catch (e) {
|
||||
localStorage.removeItem(keyId);
|
||||
console.error('Passphrases.getFromLocalStorage failed', e);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const canUseLocalStorage = (key) => getKeyId(key) !== undefined;
|
||||
|
||||
const getKeyId = (key) => {
|
||||
if (key && typeof key.id === 'string' && key.id.length > 4 && typeof key.forgetPass === 'function') {
|
||||
// only deal with keys that we can forget (OpenPGB, GnuPG)
|
||||
return key.id + '_local_key';
|
||||
} else {
|
||||
console.info('Passphrases.getKeyId: unsupported key type');
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const deriveKeyFromHash = async (hash, salt) => {
|
||||
if (!hash) {
|
||||
throw new Error('empty accountHash');
|
||||
}
|
||||
return window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(hash),
|
||||
{ 'name': 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
).then(keyMaterial => {
|
||||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
'name': 'PBKDF2',
|
||||
'salt': salt,
|
||||
'iterations': 512,
|
||||
'hash': 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ 'name': 'AES-GCM', 'length': 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
});
|
||||
};
|
||||
Passphrases.ask = async (key, sAskDesc, btnText) =>
|
||||
Passphrases.has(key)
|
||||
? {password:Passphrases.get(key)/*, remember:false*/}
|
||||
: await AskPopupView.password(sAskDesc, btnText);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
|||
|
||||
export const AccountUserStore = koArrayWithDestroy();
|
||||
|
||||
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
|
||||
|
||||
addObservablesTo(AccountUserStore, {
|
||||
email: '',
|
||||
loading: false
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ ContactUserStore.sync = fResultFunc => {
|
|||
line = JSON.parse(line);
|
||||
if ('ContactsSync' === line.Action) {
|
||||
ContactUserStore.syncing(false);
|
||||
fResultFunc?.(line.code, line);
|
||||
fResultFunc?.(line.ErrorCode, line);
|
||||
}
|
||||
} catch (e) {
|
||||
ContactUserStore.syncing(false);
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
|||
|
||||
import { Passphrases } from 'Storage/Passphrases';
|
||||
|
||||
import { baseCollator } from 'Common/Translator';
|
||||
|
||||
const
|
||||
findGnuPGKey = (keys, query/*, sign*/) =>
|
||||
keys.find(key =>
|
||||
|
|
@ -51,20 +49,13 @@ export const GnuPGUserStore = new class {
|
|||
key.for = email => aEmails.includes(IDN.toASCII(email));
|
||||
key.askDelete = ko.observable(false);
|
||||
key.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||
key.askForgetPass = ko.observable(false);
|
||||
key.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
|
||||
key.forgetPass = () => {
|
||||
Passphrases.delete(key);
|
||||
key.hasStoredPass(false);
|
||||
};
|
||||
key.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(key));
|
||||
key.remove = () => {
|
||||
if (key.askDelete()) {
|
||||
Remote.request('GnupgDeleteKey',
|
||||
(iError, oData) => {
|
||||
if (oData) {
|
||||
if (iError) {
|
||||
alert(oData.message);
|
||||
alert(oData.ErrorMessage);
|
||||
} else if (oData.Result) {
|
||||
isPrivate
|
||||
? this.privateKeys.remove(key)
|
||||
|
|
@ -76,7 +67,6 @@ export const GnuPGUserStore = new class {
|
|||
isPrivate: isPrivate
|
||||
}
|
||||
);
|
||||
isPrivate && key.forgetPass();
|
||||
}
|
||||
};
|
||||
if (isPrivate) {
|
||||
|
|
@ -85,7 +75,7 @@ export const GnuPGUserStore = new class {
|
|||
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
|
||||
btnTxt
|
||||
);
|
||||
pass && pass.remember && Passphrases.handle(key, pass.password);
|
||||
pass && pass.remember && Passphrases.set(key, pass.password);
|
||||
return pass?.password;
|
||||
};
|
||||
}
|
||||
|
|
@ -116,7 +106,7 @@ export const GnuPGUserStore = new class {
|
|||
key.view = () => key.fetch(() => showScreenPopup(OpenPgpKeyPopupView, [key]));
|
||||
return key;
|
||||
},
|
||||
collator = baseCollator(),
|
||||
collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
|
||||
sort = keys => keys.sort(
|
||||
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
|
||||
);
|
||||
|
|
@ -173,8 +163,8 @@ export const GnuPGUserStore = new class {
|
|||
return fingerprints;
|
||||
}
|
||||
|
||||
getPrivateKeyFor(query/*, sign*/) {
|
||||
return findGnuPGKey(this.privateKeys, query/*, sign*/);
|
||||
getPrivateKeyFor(query, sign) {
|
||||
return findGnuPGKey(this.privateKeys, query, sign);
|
||||
}
|
||||
|
||||
async decrypt(message) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import { koArrayWithDestroy, koComputable } from 'External/ko';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { koArrayWithDestroy } from 'External/ko';
|
||||
|
||||
export const IdentityUserStore = koArrayWithDestroy();
|
||||
|
||||
IdentityUserStore.loading = ko.observable(false).extend({ debounce: 100 });
|
||||
|
||||
/** Returns main (login) identity */
|
||||
IdentityUserStore.main = koComputable(() => {
|
||||
const list = IdentityUserStore();
|
||||
return isArray(list) ? list.find(item => item && !item.id()) : null;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,11 +32,8 @@ import { SettingsGet } from 'Common/Globals';
|
|||
import { SUB_QUERY_PREFIX } from 'Common/Links';
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
|
||||
import { baseCollator } from 'Common/Translator';
|
||||
|
||||
const
|
||||
isChecked = item => item.checked(),
|
||||
isDeleted = item => item.isDeleted(),
|
||||
replaceHash = hash => {
|
||||
rl.route.off();
|
||||
hasher.replaceHash(hash);
|
||||
|
|
@ -108,8 +105,9 @@ addComputablesTo(MessagelistUserStore, {
|
|||
listCheckedOrSelected: () => {
|
||||
const
|
||||
selectedMessage = MessagelistUserStore.selectedMessage(),
|
||||
focusedMessage = MessagelistUserStore.focusedMessage(),
|
||||
checked = MessagelistUserStore.filter(item => isChecked(item));
|
||||
return checked.length ? checked : (selectedMessage ? [selectedMessage] : []);
|
||||
return checked.length ? checked : (selectedMessage || focusedMessage ? [selectedMessage || focusedMessage] : []);
|
||||
},
|
||||
|
||||
listCheckedOrSelectedUidsWithSubMails: () => {
|
||||
|
|
@ -137,18 +135,11 @@ MessagelistUserStore.hasChecked = koComputable(
|
|||
|
||||
MessagelistUserStore.hasCheckedOrSelected = koComputable(() =>
|
||||
!!MessagelistUserStore.selectedMessage()
|
||||
| !!MessagelistUserStore.focusedMessage()
|
||||
// Issue: not all are observed?
|
||||
| !!MessagelistUserStore.find(isChecked)
|
||||
).extend({ rateLimit: 50 });
|
||||
|
||||
MessagelistUserStore.hasCheckedOrSelectedAndDeleted = koComputable(
|
||||
() => !!MessagelistUserStore.listCheckedOrSelected().find(isDeleted)
|
||||
).extend({ rateLimit: 50 });
|
||||
|
||||
MessagelistUserStore.hasCheckedOrSelectedAndUndeleted = koComputable(
|
||||
() => !!MessagelistUserStore.listCheckedOrSelected().find(item => !item?.isDeleted())
|
||||
).extend({ rateLimit: 50 });
|
||||
|
||||
MessagelistUserStore.notifyNewMessages = (folder, newMessages) => {
|
||||
if (getFolderInboxName() === folder && arrayLength(newMessages)) {
|
||||
|
||||
|
|
@ -263,14 +254,17 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
|
||||
let flags = folderInfo.permanentFlags || [];
|
||||
if (flags.includes('\\*')) {
|
||||
/** Add Thunderbird labels */
|
||||
let i = 6;
|
||||
while (--i) {
|
||||
flags.includes('$label'+i) || flags.push('$label'+i);
|
||||
}
|
||||
/** TODO: add others by default? */
|
||||
}
|
||||
folder.permanentFlags(flags.sort(baseCollator().compare));
|
||||
flags.sort((a, b) => {
|
||||
a = a.toUpperCase();
|
||||
b = b.toUpperCase();
|
||||
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
||||
});
|
||||
folder.permanentFlags(flags);
|
||||
|
||||
MessagelistUserStore.notifyNewMessages(folder.fullName, collection.newMessages);
|
||||
}
|
||||
|
|
@ -320,7 +314,6 @@ MessagelistUserStore.reload = (bDropPagePosition = false, bDropCurrentFolderCach
|
|||
|
||||
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
||||
params.useThreads = 1;
|
||||
params.threadAlgorithm = SettingsUserStore.threadAlgorithm();
|
||||
params.threadUid = MessagelistUserStore.threadUid();
|
||||
} else {
|
||||
params.threadUid = 0;
|
||||
|
|
@ -377,14 +370,6 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
|||
messages.forEach(oMessage =>
|
||||
oMessage.isFlagged() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\flagged')
|
||||
);
|
||||
} else if (iSetAction == MessageSetAction.SetDeleted) {
|
||||
messages.forEach(oMessage =>
|
||||
!oMessage.isDeleted() && rootUids.push(oMessage.uid) && oMessage.flags.push('\\deleted')
|
||||
);
|
||||
} else if (iSetAction == MessageSetAction.UnsetDeleted) {
|
||||
messages.forEach(oMessage =>
|
||||
oMessage.isDeleted() && rootUids.push(oMessage.uid) && oMessage.flags.remove('\\deleted')
|
||||
);
|
||||
}
|
||||
rootUids = rootUids.validUnique();
|
||||
length = rootUids.length;
|
||||
|
|
@ -414,15 +399,6 @@ MessagelistUserStore.setAction = (sFolderFullName, iSetAction, messages) => {
|
|||
setAction: iSetAction == MessageSetAction.SetFlag ? 1 : 0
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageSetAction.SetDeleted:
|
||||
case MessageSetAction.UnsetDeleted:
|
||||
Remote.request('MessageSetDeleted', null, {
|
||||
folder: sFolderFullName,
|
||||
uids: rootUids.join(','),
|
||||
setAction: iSetAction == MessageSetAction.SetDeleted ? 1 : 0
|
||||
});
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
}
|
||||
|
|
@ -468,6 +444,23 @@ MessagelistUserStore.moveMessages = (
|
|||
if (page > MessagelistUserStore.pageCount()) {
|
||||
setPage = MessagelistUserStore.pageCount();
|
||||
}
|
||||
if (MessagelistUserStore.threadUid()
|
||||
&& MessagelistUserStore.length
|
||||
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
||||
) {
|
||||
const message = MessagelistUserStore.find(item => item && !item.deleted());
|
||||
if (!message) {
|
||||
if (1 < page) {
|
||||
setPage = page - 1;
|
||||
} else {
|
||||
MessagelistUserStore.threadUid(0);
|
||||
setPage = MessagelistUserStore.pageBeforeThread();
|
||||
}
|
||||
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
||||
MessagelistUserStore.threadUid(message.uid);
|
||||
setPage = page;
|
||||
}
|
||||
}
|
||||
if (setPage) {
|
||||
MessagelistUserStore.page(setPage);
|
||||
replaceHash(
|
||||
|
|
@ -525,6 +518,7 @@ MessagelistUserStore.moveMessages = (
|
|||
currentMessage = null;
|
||||
MessageUserStore.message(null);
|
||||
}
|
||||
item.deleted(true);
|
||||
MessagelistUserStore.remove(item);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
|
|||
|
||||
import { Passphrases } from 'Storage/Passphrases';
|
||||
|
||||
import { baseCollator, getNotification } from 'Common/Translator';
|
||||
|
||||
const
|
||||
loaded = () => !!window.openpgp,
|
||||
|
||||
findOpenPGPKey = (keys, query/*, sign*/) =>
|
||||
keys.find(key =>
|
||||
key.for(query) || query == key.id || query == key.fingerprint
|
||||
|
|
@ -38,12 +34,12 @@ const
|
|||
privateKey: privateKey.key,
|
||||
passphrase
|
||||
});
|
||||
result && pass.remember && Passphrases.handle(privateKey, passphrase);
|
||||
result && pass.remember && Passphrases.set(privateKey, passphrase);
|
||||
return result;
|
||||
}
|
||||
},
|
||||
|
||||
collator = baseCollator(),
|
||||
collator = new Intl.Collator(undefined, {sensitivity: 'base'}),
|
||||
sort = keys => keys.sort(
|
||||
// (a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.fingerprint, b.fingerprint)
|
||||
(a, b) => collator.compare(a.emails[0], b.emails[0]) || collator.compare(a.id, b.id)
|
||||
|
|
@ -58,9 +54,10 @@ const
|
|||
*/
|
||||
publicKeysItem = 'openpgp-public-keys',
|
||||
privateKeysItem = 'openpgp-private-keys',
|
||||
storage = window.localStorage,
|
||||
loadOpenPgpKeys = async itemname => {
|
||||
let keys = [], key,
|
||||
armoredKeys = JSON.parse(localStorage.getItem(itemname)),
|
||||
armoredKeys = JSON.parse(storage.getItem(itemname)),
|
||||
i = arrayLength(armoredKeys);
|
||||
while (i--) try {
|
||||
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
|
||||
|
|
@ -73,9 +70,9 @@ const
|
|||
storeOpenPgpKeys = (keys, section) => {
|
||||
let armoredKeys = keys.map(item => item.armor);
|
||||
if (armoredKeys.length) {
|
||||
localStorage.setItem(section, JSON.stringify(armoredKeys));
|
||||
storage.setItem(section, JSON.stringify(armoredKeys));
|
||||
} else {
|
||||
localStorage.removeItem(section);
|
||||
storage.removeItem(section);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -90,9 +87,6 @@ class OpenPgpKeyModel {
|
|||
this.armor = armor;
|
||||
this.askDelete = ko.observable(false);
|
||||
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||
this.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(this));
|
||||
this.askForgetPass = ko.observable(false);
|
||||
this.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
|
||||
// key.getUserIDs()
|
||||
// key.getPrimaryUser()
|
||||
}
|
||||
|
|
@ -118,29 +112,12 @@ class OpenPgpKeyModel {
|
|||
if (this.key.isPrivate()) {
|
||||
OpenPGPUserStore.privateKeys.remove(this);
|
||||
storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
|
||||
this.forgetPass()
|
||||
} else {
|
||||
OpenPGPUserStore.publicKeys.remove(this);
|
||||
storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem);
|
||||
}
|
||||
Remote.request('DeletePGPKey',
|
||||
(iError, oData) => {
|
||||
if (oData) {
|
||||
if (iError || oData.Result === false) {
|
||||
alert(oData.message || getNotification(iError));
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: this.armor
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
forgetPass() {
|
||||
Passphrases.delete(this);
|
||||
this.hasStoredPass(false);
|
||||
}
|
||||
/*
|
||||
toJSON() {
|
||||
return this.armor;
|
||||
|
|
@ -155,36 +132,23 @@ export const OpenPGPUserStore = new class {
|
|||
}
|
||||
|
||||
loadKeyrings() {
|
||||
if (loaded()) {
|
||||
loadOpenPgpKeys(publicKeysItem)
|
||||
.then(keys => {
|
||||
if (window.openpgp) {
|
||||
loadOpenPgpKeys(publicKeysItem).then(keys => {
|
||||
this.publicKeys(dedup(keys));
|
||||
console.log('openpgp.js public keys loaded');
|
||||
})
|
||||
.finally(() => {
|
||||
loadOpenPgpKeys(privateKeysItem)
|
||||
.then(keys => {
|
||||
this.privateKeys(dedup(keys));
|
||||
console.log('openpgp.js private keys loaded');
|
||||
})
|
||||
.finally(() => {
|
||||
/*SettingsGet('loadBackupKeys') && */this.loadBackupKeys();
|
||||
});
|
||||
});
|
||||
loadOpenPgpKeys(privateKeysItem).then(keys => {
|
||||
this.privateKeys(dedup(keys));
|
||||
console.log('openpgp.js private keys loaded');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadBackupKeys() {
|
||||
Remote.request('GetPGPKeys',
|
||||
(iError, oData) => !iError && oData.Result && this.importKeys(oData.Result)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSupported() {
|
||||
return loaded();
|
||||
return !!window.openpgp;
|
||||
}
|
||||
|
||||
importKey(armoredKey) {
|
||||
|
|
@ -192,7 +156,7 @@ export const OpenPGPUserStore = new class {
|
|||
}
|
||||
|
||||
async importKeys(keys) {
|
||||
if (loaded()) {
|
||||
if (window.openpgp) {
|
||||
const privateKeys = this.privateKeys(),
|
||||
publicKeys = this.publicKeys();
|
||||
for (const armoredKey of keys) try {
|
||||
|
|
@ -219,7 +183,7 @@ export const OpenPGPUserStore = new class {
|
|||
keyPair.revocationCertificate
|
||||
*/
|
||||
storeKeyPair(keyPair) {
|
||||
if (loaded()) {
|
||||
if (window.openpgp) {
|
||||
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
|
||||
this.publicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
|
||||
storeOpenPgpKeys(this.publicKeys, publicKeysItem);
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const
|
|||
if (gnuPG && oData?.Result/* && (oData.Result.imported || oData.Result.secretimported)*/) {
|
||||
GnuPGUserStore.loadKeyrings();
|
||||
}
|
||||
iError && alert(oData.message);
|
||||
iError && alert(oData.ErrorMessage);
|
||||
}, {
|
||||
key, gnuPG, backup
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||
import { baseCollator } from 'Common/Translator';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export const SMimeUserStore = koArrayWithDestroy();
|
||||
|
|
@ -13,7 +12,7 @@ SMimeUserStore.loadCertificates = () => {
|
|||
SMimeUserStore.loading(true);
|
||||
Remote.request('SMimeGetCertificates', (iError, oData) => {
|
||||
SMimeUserStore.loading(false);
|
||||
const collator = baseCollator();
|
||||
const collator = new Intl.Collator(undefined, {sensitivity: 'base'});
|
||||
iError || SMimeUserStore(oData.Result.sort(
|
||||
(a, b) => collator.compare(a.emailAddress, b.emailAddress) || (b.validTo_time_t - a.validTo_time_t)
|
||||
));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
|||
import { pInt } from 'Common/Utils';
|
||||
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
|
||||
export const SettingsUserStore = new class {
|
||||
constructor() {
|
||||
|
|
@ -23,7 +22,6 @@ export const SettingsUserStore = new class {
|
|||
allowStyles: 0,
|
||||
collapseBlockquotes: 1,
|
||||
maxBlockquotesLevel: 0,
|
||||
minFontSize: 0,
|
||||
listInlineAttachments: 0,
|
||||
simpleAttachmentsList: 0,
|
||||
useCheckboxesInList: 1,
|
||||
|
|
@ -31,14 +29,11 @@ export const SettingsUserStore = new class {
|
|||
showNextMessage: 0,
|
||||
allowDraftAutosave: 1,
|
||||
useThreads: 0,
|
||||
defaultSort: '',
|
||||
threadAlgorithm: '',
|
||||
replySameFolder: 0,
|
||||
hideUnsubscribed: 0,
|
||||
hideDeleted: 1,
|
||||
unhideKolabFolders: 0,
|
||||
autoLogout: 0,
|
||||
keyPassForget: 15,
|
||||
showUnreadCount: 0,
|
||||
messageNewWindow: 0,
|
||||
messageReadAuto: 0,
|
||||
|
|
@ -53,7 +48,6 @@ export const SettingsUserStore = new class {
|
|||
layout: 1,
|
||||
editorDefaultType: 'Html',
|
||||
editorWysiwyg: 'Squire',
|
||||
markdown: 0,
|
||||
msgDefaultAction: 1
|
||||
});
|
||||
|
||||
|
|
@ -99,16 +93,13 @@ export const SettingsUserStore = new class {
|
|||
'AllowStyles',
|
||||
'CollapseBlockquotes',
|
||||
'MaxBlockquotesLevel',
|
||||
'minFontSize',
|
||||
'ListInlineAttachments',
|
||||
'simpleAttachmentsList',
|
||||
'UseCheckboxesInList',
|
||||
'listGrouped',
|
||||
'showNextMessage',
|
||||
'AllowDraftAutosave',
|
||||
'useThreads',
|
||||
'defaultSort',
|
||||
'threadAlgorithm',
|
||||
'UseThreads',
|
||||
'ReplySameFolder',
|
||||
'HideUnsubscribed',
|
||||
'HideDeleted',
|
||||
|
|
@ -119,41 +110,7 @@ export const SettingsUserStore = new class {
|
|||
'requireTLS',
|
||||
'pgpSign',
|
||||
'pgpEncrypt',
|
||||
'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',
|
||||
*/
|
||||
'allowSpellcheck'
|
||||
].forEach(name => {
|
||||
let value = SettingsGet(name);
|
||||
name = name[0].toLowerCase() + name.slice(1);
|
||||
|
|
@ -165,8 +122,5 @@ export const SettingsUserStore = new class {
|
|||
self.checkMailInterval(pInt(SettingsGet('CheckMailInterval')));
|
||||
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
|
||||
self.autoLogout(pInt(SettingsGet('AutoLogout')));
|
||||
self.keyPassForget(pInt(SettingsGet('keyPassForget')));
|
||||
|
||||
FolderUserStore.sortMode(self.defaultSort());
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,9 +34,4 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
grid-column-end: 3;
|
||||
border: 1px solid var(--border-color, #ddd);
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#V-PopupsLanguages {
|
||||
|
||||
max-width: 720px;
|
||||
max-width: 710px;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@
|
|||
#V-PopupsCompose {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-width: min(99vw, 1600px);
|
||||
resize: horizontal;
|
||||
max-width: 1000px;
|
||||
width: 99%;
|
||||
|
||||
.modal-body {
|
||||
|
|
@ -22,18 +21,12 @@
|
|||
|
||||
.tabs {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tabs > label > * + * {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.tabs .tab-content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.textAreaParent, .attachmentAreaParent {
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
|
|
|
|||
|
|
@ -125,9 +125,8 @@
|
|||
/* Right */
|
||||
|
||||
.right {
|
||||
border-left: 1px solid var(--border-color, #ddd);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--border-color, #ddd);
|
||||
}
|
||||
|
||||
.b-view-content-toolbar {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
border-left: 3px solid transparent;
|
||||
color: var(--folders-color, #333);
|
||||
display: block;
|
||||
line-height: 2.2em;
|
||||
line-height: 34px;
|
||||
padding: 0 2em 0 @folderItemPadding;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
color: var(--unread-count-color, #fff);
|
||||
font-size: 80%;
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.5em;
|
||||
margin-top: 7px;
|
||||
min-width: 1.7em;
|
||||
padding: 1px 4px;
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -207,6 +207,10 @@
|
|||
font-weight: bolder;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.messageCheckbox {
|
||||
font-size: 114%;
|
||||
padding: 5px .5em 0;
|
||||
|
|
@ -323,7 +327,6 @@
|
|||
.attachmentParent,
|
||||
.flagParent {
|
||||
margin: 0 10px 0 5px;
|
||||
min-width: 1em;
|
||||
}
|
||||
.flagParent::after {
|
||||
content: '☆'; /*⚐*/
|
||||
|
|
|
|||
|
|
@ -45,8 +45,4 @@
|
|||
content: ')';
|
||||
}
|
||||
|
||||
.identities-list tr + tr .identity-default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,12 +56,6 @@
|
|||
.squire-wysiwyg {
|
||||
font-family: var(--fontSans);
|
||||
|
||||
/* Prevent collapse of empty elements so that the caret can be placed in there
|
||||
*:empty::before {
|
||||
content: "\200B";
|
||||
}
|
||||
*/
|
||||
|
||||
ul {
|
||||
padding-left: 40px;
|
||||
li {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
text-align: var(--left, left);
|
||||
top: 100%;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
white-space: pre;
|
||||
white-space: nowrap;
|
||||
z-index: 2001;
|
||||
}
|
||||
@media screen and (max-width: @maxMobileWidth) {
|
||||
|
|
@ -33,11 +33,6 @@ input:-webkit-autofill:active {
|
|||
transition: color 9999s ease-out, background-color 9999s ease-out;
|
||||
}
|
||||
|
||||
.btn-rotate,
|
||||
[data-icon].rotate::before {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
/*
|
||||
@media (pointer: coarse), (hover: none) {
|
||||
[title] {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class AccountPopupView extends AbstractViewPopup {
|
|||
this.submitRequest(false);
|
||||
if (iError) {
|
||||
this.submitError(getNotification(iError));
|
||||
this.submitErrorAdditional(data?.messageAdditional);
|
||||
this.submitErrorAdditional(data?.ErrorMessageAdditional);
|
||||
} else {
|
||||
loadAccountsAndIdentities();
|
||||
this.close();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
|
|||
text: '',
|
||||
keyword: '',
|
||||
repliedValue: -1,
|
||||
selectedDateValue: 0,
|
||||
selectedDateValue: -1,
|
||||
selectedTreeValue: '',
|
||||
|
||||
hasAttachment: false,
|
||||
|
|
@ -32,12 +32,14 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
|
|||
// Almost the same as MessageModel.tagOptions
|
||||
keywords: () => {
|
||||
const keywords = [{value:'',label:''}];
|
||||
FolderUserStore.currentFolder().optionalTags().forEach(value => {
|
||||
let lower = value.toLowerCase();
|
||||
keywords.push({
|
||||
value: value,
|
||||
label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
|
||||
});
|
||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
||||
if (isAllowedKeyword(value)) {
|
||||
let lower = value.toLowerCase();
|
||||
keywords.push({
|
||||
value: value,
|
||||
label: i18n('MESSAGE_TAGS/'+lower, 0, lower)
|
||||
});
|
||||
}
|
||||
});
|
||||
return keywords
|
||||
},
|
||||
|
|
@ -55,25 +57,15 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
|
|||
|
||||
selectedDates: () => {
|
||||
translateTrigger();
|
||||
// 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_';
|
||||
let prefix = 'SEARCH/DATE_';
|
||||
return [
|
||||
{ id: 0, name: i18n('SEARCH/DATE_ALL') },
|
||||
{ id: -1, name: i18n(prefix + 'ALL') },
|
||||
{ id: 3, name: i18n(prefix + '3_DAYS') },
|
||||
{ id: 7, name: i18n(prefix + '7_DAYS') },
|
||||
{ id: 30, name: i18n(prefix + 'MONTH') },
|
||||
{ id: 90, name: i18n(prefix + '3_MONTHS') },
|
||||
{ id: 180, name: i18n(prefix + '6_MONTHS') },
|
||||
{ id: 365, name: i18n(prefix + 'YEAR') },
|
||||
{ id: -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') }
|
||||
{ id: 365, name: i18n(prefix + 'YEAR') }
|
||||
];
|
||||
},
|
||||
|
||||
|
|
@ -110,16 +102,11 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
|
|||
append('text', self.text().trim());
|
||||
append('keyword', self.keyword());
|
||||
append('in', self.selectedTreeValue());
|
||||
if (0 < self.selectedDateValue()) {
|
||||
if (-1 < self.selectedDateValue()) {
|
||||
let d = new Date();
|
||||
d.setDate(d.getDate() - self.selectedDateValue());
|
||||
append('since', d.toISOString().split('T')[0]);
|
||||
}
|
||||
else if (-1 > self.selectedDateValue()) {
|
||||
let d = new Date();
|
||||
d.setDate(d.getDate() + self.selectedDateValue());
|
||||
append('before', d.toISOString().split('T')[0]);
|
||||
}
|
||||
|
||||
let result = decodeURIComponent(new URLSearchParams(data).toString());
|
||||
|
||||
|
|
@ -151,7 +138,8 @@ export class AdvancedSearchPopupView extends AbstractViewPopup {
|
|||
self.text(pString(params.get('text')));
|
||||
self.keyword(pString(params.get('keyword')));
|
||||
self.selectedTreeValue(pString(params.get('in')));
|
||||
self.selectedDateValue(0);
|
||||
// self.selectedDateValue(params.get('since'));
|
||||
self.selectedDateValue(-1);
|
||||
self.hasAttachment(params.has('attachment'));
|
||||
self.starred(params.has('flagged'));
|
||||
self.unseen(params.has('unseen'));
|
||||
|
|
|
|||
|
|
@ -15,14 +15,10 @@ export class AskPopupView extends AbstractViewPopup {
|
|||
askUsername: false,
|
||||
passphrase: '',
|
||||
askPass: false,
|
||||
remember: true, // remember for session
|
||||
askRemeber: false,
|
||||
rememberPermanent: false,
|
||||
askRememberPermanent: false,
|
||||
remember: true,
|
||||
askRemeber: false
|
||||
});
|
||||
|
||||
this.rememberPermanent.subscribe(value => value && this.remember(true));
|
||||
|
||||
this.fYesAction = null;
|
||||
this.fNoAction = null;
|
||||
|
||||
|
|
@ -52,12 +48,10 @@ export class AskPopupView extends AbstractViewPopup {
|
|||
this.askDesc(sAskDesc || '');
|
||||
this.askUsername(ask & 2);
|
||||
this.askPass(ask & 1);
|
||||
this.askRemeber(ask & 4); // 0b0100
|
||||
this.askRememberPermanent(ask & 0b1000)
|
||||
this.askRemeber(ask & 4);
|
||||
this.username('');
|
||||
this.passphrase('');
|
||||
this.remember(true);
|
||||
this.rememberPermanent(false);
|
||||
this.yesButton(i18n(btnText || 'GLOBAL/YES'));
|
||||
this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
|
||||
this.fYesAction = fYesFunc;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import Remote from 'Remote/User/Fetch';
|
|||
|
||||
import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { IdentityModel } from 'Model/Identity';
|
||||
import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
|
||||
import { addressparser } from 'Mime/Address';
|
||||
|
||||
|
|
@ -93,7 +92,7 @@ const
|
|||
|
||||
findIdentity = addresses => {
|
||||
addresses = addresses.map(item => item.email);
|
||||
return IdentityUserStore.find(item => addresses.includes(item.email));
|
||||
return IdentityUserStore.find(item => addresses.includes(item.email()));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -299,7 +298,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
]
|
||||
});
|
||||
|
||||
this.doClose = this.doClose.debounce(200);
|
||||
this.tryToClose = this.tryToClose.debounce(200);
|
||||
|
||||
this.iTimer = 0;
|
||||
|
||||
|
|
@ -337,14 +336,11 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
|
||||
canSign: () => this.signOptions().length,
|
||||
|
||||
encryptOptionsText: () => this.encryptOptions().join(', '),
|
||||
signOptionsText: () => this.signOptions().map(o => o[0]).join(', '),
|
||||
|
||||
identitiesOptions: () =>
|
||||
IdentityUserStore.map(item => ({
|
||||
item: item,
|
||||
optValue: item.id(),
|
||||
optText: item
|
||||
optText: item.formattedName()
|
||||
})),
|
||||
|
||||
canBeSentOrSaved: () => !this.sending() && !this.saving()
|
||||
|
|
@ -359,7 +355,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
|
||||
currentIdentity: value => {
|
||||
if (value) {
|
||||
this.from(value.toString());
|
||||
this.from(value.formattedName());
|
||||
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
|
||||
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
|
||||
}
|
||||
|
|
@ -455,24 +451,13 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
contactsCommand: self => self.allowContacts
|
||||
});
|
||||
|
||||
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;
|
||||
this.from(IdentityUserStore()[0].formattedName());
|
||||
}
|
||||
|
||||
sendCommand() {
|
||||
const identity = this.currentIdentity();
|
||||
let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
|
||||
|
||||
this.attachmentsInProcessError(false);
|
||||
this.attachmentsInErrorError(false);
|
||||
this.emptyToError(false);
|
||||
|
|
@ -490,95 +475,80 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
}
|
||||
|
||||
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
|
||||
const sSentFolder = this.sentFolder();
|
||||
if ('' === sSentFolder) {
|
||||
if (SettingsUserStore.replySameFolder()) {
|
||||
if (
|
||||
3 === arrayLength(this.aDraftInfo) &&
|
||||
null != this.aDraftInfo[2] &&
|
||||
this.aDraftInfo[2].length
|
||||
) {
|
||||
sSentFolder = this.aDraftInfo[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (!sSentFolder) {
|
||||
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
|
||||
} else {
|
||||
const sendError = e => {
|
||||
} else try {
|
||||
this.sendError(false);
|
||||
this.sending(true);
|
||||
|
||||
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
|
||||
|
||||
this.getMessageRequestParams(sSentFolder).then(params => {
|
||||
Remote.request('SendMessage',
|
||||
(iError, data) => {
|
||||
this.sending(false);
|
||||
if (iError) {
|
||||
if (Notifications.CantSaveMessage === iError) {
|
||||
this.sendSuccessButSaveError(true);
|
||||
let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
|
||||
if (data?.ErrorMessageAdditional) {
|
||||
msg = msg + "\n" + data?.ErrorMessageAdditional;
|
||||
}
|
||||
this.savedErrorDesc(msg);
|
||||
} else {
|
||||
params.signPassphrase && Passphrases.delete(identity);
|
||||
this.sendError(true);
|
||||
this.sendErrorDesc(getNotification(iError, data?.ErrorMessage)
|
||||
|| getNotification(Notifications.CantSendMessage));
|
||||
}
|
||||
} else {
|
||||
if (arrayLength(this.aDraftInfo) > 0) {
|
||||
const flag = {
|
||||
'reply': '\\answered',
|
||||
'forward': '$forwarded'
|
||||
}[this.aDraftInfo[0]];
|
||||
if (flag) {
|
||||
const aFlags = oLastMessage.flags();
|
||||
if (aFlags.indexOf(flag) === -1) {
|
||||
aFlags.push(flag);
|
||||
oLastMessage.flags(aFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
setFolderETag(this.draftsFolder(), '');
|
||||
setFolderETag(sSentFolder, '');
|
||||
if (3 === arrayLength(this.aDraftInfo)) {
|
||||
const folder = this.aDraftInfo[2];
|
||||
setFolderETag(folder, '');
|
||||
}
|
||||
reloadDraftFolder();
|
||||
},
|
||||
params,
|
||||
30000
|
||||
);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
this.sendError(true);
|
||||
this.sendErrorDesc(e);
|
||||
this.sending(false);
|
||||
};
|
||||
const sendFailed = (iError, data) => {
|
||||
this.sendError(true);
|
||||
this.sendErrorDesc(
|
||||
getNotification(iError, data?.message, Notifications.CantSendMessage)
|
||||
+ "\n" + (data?.messageAdditional || data?.message)
|
||||
);
|
||||
};
|
||||
try {
|
||||
this.sendError(false);
|
||||
this.sending(true);
|
||||
|
||||
const sendMessage = params => {
|
||||
Remote.request('SendMessage',
|
||||
(iError, data) => {
|
||||
this.sending(false);
|
||||
if (iError) {
|
||||
/*
|
||||
if (Notifications.AuthError === iError && !params.auth) {
|
||||
AskPopupView.password('SMTP login', 'retry', 3).then(result => {
|
||||
if (result) {
|
||||
this.sending(true);
|
||||
params.auth = result;
|
||||
sendMessage(params);
|
||||
} else {
|
||||
sendFailed(iError, data);
|
||||
}
|
||||
});
|
||||
} else
|
||||
*/
|
||||
if (Notifications.CantSaveMessage === iError) {
|
||||
this.sendSuccessButSaveError(true);
|
||||
let msg = i18n('COMPOSE/SAVED_ERROR_ON_SEND');
|
||||
if (data?.messageAdditional) {
|
||||
msg = msg + "\n" + data?.messageAdditional;
|
||||
}
|
||||
this.savedErrorDesc(msg);
|
||||
} else {
|
||||
this.sendError(true);
|
||||
sendFailed(iError, data);
|
||||
// Remove remembered passphrase as it could be wrong
|
||||
let key = ('S/MIME' === params.sign) ? this.currentIdentity() : null;
|
||||
params.signFingerprint
|
||||
&& this.signOptions.forEach(option => ('GnuPG' === option[0]) && (key = option[1]));
|
||||
key && Passphrases.delete(key);
|
||||
}
|
||||
} else {
|
||||
if (arrayLength(this.aDraftInfo) > 0) {
|
||||
const flag = {
|
||||
'reply': '\\answered',
|
||||
'forward': '$forwarded'
|
||||
}[this.aDraftInfo[0]];
|
||||
if (flag) {
|
||||
const aFlags = oLastMessage.flags();
|
||||
if (aFlags.indexOf(flag) === -1) {
|
||||
aFlags.push(flag);
|
||||
oLastMessage.flags(aFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
setFolderETag(this.draftsFolder(), '');
|
||||
setFolderETag(params.saveFolder, '');
|
||||
if (3 === arrayLength(this.aDraftInfo)) {
|
||||
setFolderETag(this.aDraftInfo[2], '');
|
||||
}
|
||||
reloadDraftFolder();
|
||||
},
|
||||
params,
|
||||
30000
|
||||
);
|
||||
};
|
||||
|
||||
this.getMessageRequestParams(sSentFolder)
|
||||
.then(sendMessage)
|
||||
.catch(sendError);
|
||||
} catch (e) {
|
||||
sendError(e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.sendError(true);
|
||||
this.sendErrorDesc(e);
|
||||
this.sending(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -666,7 +636,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
this.saveCommand();
|
||||
}
|
||||
|
||||
this.doClose();
|
||||
this.tryToClose();
|
||||
}
|
||||
|
||||
contactsCommand() {
|
||||
|
|
@ -863,7 +833,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
case ComposeType.Forward:
|
||||
case ComposeType.ForwardAsAttachment:
|
||||
identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc))
|
||||
|| findIdentity(oLastMessage.from)
|
||||
/* || findIdentity(oLastMessage.deliveredTo)*/;
|
||||
break;
|
||||
case ComposeType.Draft:
|
||||
|
|
@ -873,20 +842,10 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
// case ComposeType.Empty:
|
||||
}
|
||||
}
|
||||
// Set from custom email
|
||||
if (!identity
|
||||
&& oLastMessage && (ComposeType.Reply === options.mode || ComposeType.ReplyAll === options.mode)
|
||||
&& 1 === oLastMessage.to.length
|
||||
// && mEmail.includes(oLastMessage.to[0].domain)
|
||||
) {
|
||||
identity = new IdentityModel;
|
||||
identity.name = oLastMessage.to[0].name;
|
||||
identity.email = oLastMessage.to[0].email;
|
||||
}
|
||||
identity = identity || IdentityUserStore()[0];
|
||||
if (identity) {
|
||||
// excludeEmail.add(identity.email);
|
||||
excludeEmail[identity.email] = true;
|
||||
// excludeEmail.add(identity.email());
|
||||
excludeEmail[identity.email()] = true;
|
||||
}
|
||||
|
||||
if (arrayLength(options.to)) {
|
||||
|
|
@ -902,7 +861,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
}
|
||||
|
||||
if (options.mode && oLastMessage) {
|
||||
let usePlain,
|
||||
let encrypted,
|
||||
sCc = '',
|
||||
sDate = timestampToString(oLastMessage.dateTimestamp(), 'FULL'),
|
||||
sSubject = oLastMessage.subject(),
|
||||
|
|
@ -930,9 +889,10 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
oLastMessage.headers().valuesByName('autocrypt').forEach(value => {
|
||||
let autocrypt = new MimeHeaderAutocryptModel(value);
|
||||
if (autocrypt.addr && autocrypt.keydata) {
|
||||
PgpUserStore.hasPublicKeyForEmails([autocrypt.addr])
|
||||
|| PgpUserStore.importKey(autocrypt.pem(), true, true)
|
||||
// || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
|
||||
PgpUserStore.hasPublicKeyForEmails([autocrypt.addr]).then(result =>
|
||||
result || PgpUserStore.importKey(autocrypt.pem(), true, true)
|
||||
// result || showScreenPopup(OpenPgpImportPopupView, [autocrypt.pem()])
|
||||
);
|
||||
}
|
||||
});
|
||||
} break;
|
||||
|
|
@ -971,7 +931,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
// https://github.com/the-djmaze/snappymail/issues/491
|
||||
tpl.innerHTML = oLastMessage.bodyAsHTML();
|
||||
tpl.content.querySelectorAll('img').forEach(img => {
|
||||
img.src || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
|
||||
img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
|
||||
});
|
||||
sText = tpl.innerHTML.trim();
|
||||
|
||||
|
|
@ -1010,14 +970,18 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
break;
|
||||
|
||||
default:
|
||||
usePlain = PgpUserStore.isEncrypted(sText) || isPlainEditor() || !oLastMessage.isHtml();
|
||||
if (usePlain) {
|
||||
encrypted = PgpUserStore.isEncrypted(sText);
|
||||
if (encrypted) {
|
||||
sText = oLastMessage.plain();
|
||||
}
|
||||
}
|
||||
|
||||
this.editor(editor => {
|
||||
usePlain ? (editor.modePlain() | editor.setPlain(sText)) : editor.setHtml(sText);
|
||||
encrypted || editor.setHtml(sText);
|
||||
if (encrypted || isPlainEditor()) {
|
||||
editor.modePlain();
|
||||
}
|
||||
encrypted && editor.setPlain(sText);
|
||||
this.setSignature(identity, options.mode);
|
||||
this.setFocusInPopup();
|
||||
});
|
||||
|
|
@ -1084,7 +1048,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
}, 100);
|
||||
}
|
||||
|
||||
doClose() {
|
||||
tryToClose() {
|
||||
if (AskPopupView.hidden()) {
|
||||
if (ComposePopupView.inEdit() || (this.isEmptyForm() && !this.draftUid())) {
|
||||
this.close();
|
||||
|
|
@ -1163,7 +1127,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
.on('onComplete', (id, result, data) => {
|
||||
const attachment = this.getAttachmentById(id),
|
||||
response = data?.Result || {},
|
||||
errorCode = response.code,
|
||||
errorCode = response.ErrorCode,
|
||||
attachmentJson = result && response.Attachment;
|
||||
|
||||
let error = '';
|
||||
|
|
@ -1179,7 +1143,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
.waiting(false)
|
||||
.uploading(false)
|
||||
.complete(true)
|
||||
.error(error + '\n' + response.message);
|
||||
.error(error + '\n' + response.ErrorMessage);
|
||||
} else if (attachmentJson) {
|
||||
attachment
|
||||
.waiting(false)
|
||||
|
|
@ -1230,7 +1194,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
});
|
||||
|
||||
addShortcut('escape,close', 'shift', ScopeCompose, () => {
|
||||
this.doClose();
|
||||
this.tryToClose();
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
@ -1393,7 +1357,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
allRecipients() {
|
||||
return [
|
||||
// From/sender is also recipient (Sent mailbox)
|
||||
// this.currentIdentity().email,
|
||||
// this.currentIdentity().email(),
|
||||
this.from(),
|
||||
this.to(),
|
||||
this.cc(),
|
||||
|
|
@ -1413,8 +1377,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
key && options.push(['OpenPGP', key]);
|
||||
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
||||
key && options.push(['GnuPG', key]);
|
||||
identity.smimeKeyValid() && identity.smimeCertificateValid()
|
||||
&& identity.smimeCertificateChainValid() && identity.email === email
|
||||
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
|
||||
&& options.push(['S/MIME']);
|
||||
console.dir({signOptions: options});
|
||||
this.signOptions(options);
|
||||
|
|
@ -1431,22 +1394,22 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
OpenPGPUserStore.hasPublicKeyForEmails(recipients)
|
||||
&& options.push('OpenPGP');
|
||||
|
||||
const count = recipients.length,
|
||||
identity = this.currentIdentity(),
|
||||
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email : null;
|
||||
count
|
||||
&& count === recipients.filter(email =>
|
||||
email == from
|
||||
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
|
||||
).length
|
||||
&& options.push('S/MIME');
|
||||
|
||||
if (await MailvelopeUserStore.hasPublicKeyForEmails(recipients)) {
|
||||
options.push('Mailvelope');
|
||||
} else {
|
||||
'mailvelope' === this.viewArea() && this.bodyArea();
|
||||
// this.dropMailvelope();
|
||||
}
|
||||
|
||||
const count = recipients.length,
|
||||
identity = this.currentIdentity(),
|
||||
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
|
||||
count
|
||||
&& count === recipients.filter(email =>
|
||||
email == from
|
||||
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
|
||||
).length
|
||||
&& options.push('S/MIME');
|
||||
}
|
||||
|
||||
console.dir({encryptOptions:options});
|
||||
|
|
@ -1510,21 +1473,12 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
**/
|
||||
linkedData: []
|
||||
},
|
||||
recipients = draft ? [identity.email] : this.allRecipients(),
|
||||
recipients = draft ? [identity.email()] : this.allRecipients(),
|
||||
signOptions = !draft && this.doSign() && this.signOptions(),
|
||||
encryptOptions = this.doEncrypt() && this.encryptOptions(),
|
||||
isHtml = this.oEditor.isHtml();
|
||||
|
||||
if (isHtml) {
|
||||
tpl.innerHTML = Text;
|
||||
tpl.content.querySelectorAll('img').forEach(img => {
|
||||
if (img.dataset.xSrc) {
|
||||
img.src = img.dataset.xSrc;
|
||||
img.removeAttribute('data-x-src')
|
||||
}
|
||||
});
|
||||
Text = tpl.innerHTML.trim();
|
||||
|
||||
do {
|
||||
l = Text.length;
|
||||
Text = Text
|
||||
|
|
@ -1568,11 +1522,12 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
alternative.children.push(data);
|
||||
data = alternative;
|
||||
}
|
||||
let isSigned = false;
|
||||
let sign = true;
|
||||
for (let i = 0; i < signOptions.length; ++i) {
|
||||
if ('OpenPGP' == signOptions[i][0]) {
|
||||
try {
|
||||
// Doesn't sign attachments
|
||||
params.html = params.plain = '';
|
||||
let signed = new MimePart;
|
||||
signed.headers['Content-Type'] =
|
||||
'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"';
|
||||
|
|
@ -1583,52 +1538,47 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
signature.headers['Content-Transfer-Encoding'] = '7Bit';
|
||||
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
|
||||
signed.children.push(signature);
|
||||
isSigned = true;
|
||||
params.html = params.plain = '';
|
||||
params.signed = signed.toString();
|
||||
params.boundary = signed.boundary;
|
||||
data = signed;
|
||||
/*
|
||||
Object.entries(PgpUserStore.getPublicKeyOfEmails([getEmail(this.from())]) || {})
|
||||
.forEach(([k,v]) => params.publicKey = v);
|
||||
*/
|
||||
break;
|
||||
} catch (e) {
|
||||
Passphrases.delete(signOptions[i][1])
|
||||
sign = false;
|
||||
console.error(e);
|
||||
}
|
||||
} else if ('GnuPG' == signOptions[i][0]) {
|
||||
break;
|
||||
}
|
||||
if ('GnuPG' == signOptions[i][0]) {
|
||||
// TODO: sign in PHP fails
|
||||
let pass = await GnuPGUserStore.sign(signOptions[i][1]);
|
||||
if (null != pass) {
|
||||
// params.signData = data.toString();
|
||||
params.signFingerprint = signOptions[i][1].fingerprint;
|
||||
params.signPassphrase = pass;
|
||||
// params.attachPublicKey = false;
|
||||
isSigned = true;
|
||||
break;
|
||||
} else {
|
||||
sign = false;
|
||||
}
|
||||
} else if ('S/MIME' == signOptions[i][0]) {
|
||||
break;
|
||||
}
|
||||
if ('S/MIME' == signOptions[i][0]) {
|
||||
// TODO: sign in PHP fails
|
||||
params.sign = 'S/MIME';
|
||||
// params.signCertificate = identity.smimeCertificate();
|
||||
// params.signCertificateChain = identity.smimeCertificateChain();
|
||||
// params.signPrivateKey = identity.smimeKey();
|
||||
// params.attachCertificate = false;
|
||||
if (identity.smimeKeyEncrypted()) {
|
||||
const pass = await Passphrases.ask(identity,
|
||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}),
|
||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||
'CRYPTO/SIGN'
|
||||
);
|
||||
if (null != pass) {
|
||||
params.signPassphrase = pass.password;
|
||||
pass.remember && Passphrases.handle(identity, pass.password);
|
||||
isSigned = true;
|
||||
// pass.remember && Passphrases.set(identity, pass.password);
|
||||
} else {
|
||||
sign = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (signOptions.length && !isSigned) {
|
||||
if (signOptions.length && !sign) {
|
||||
throw 'Signing failed';
|
||||
}
|
||||
|
||||
|
|
@ -1637,9 +1587,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
|
||||
params.autocrypt.push({
|
||||
addr: k,
|
||||
keydata: v
|
||||
.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '')
|
||||
.replace(/\s+/g, '') // Single base64 string, let the server do compliance formatting
|
||||
keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
|
||||
})
|
||||
);
|
||||
for (let i = 0; i < encryptOptions.length; ++i) {
|
||||
|
|
@ -1659,7 +1607,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
if ('S/MIME' == encryptOptions[i]) {
|
||||
params.encryptCertificates = [identity.smimeCertificate()];
|
||||
SMimeUserStore.forEach(certificate => {
|
||||
certificate.emailAddress != identity.email
|
||||
certificate.emailAddress != identity.email()
|
||||
&& recipients.includes(certificate.emailAddress)
|
||||
&& params.encryptCertificates.push(certificate.id)
|
||||
});
|
||||
|
|
|
|||
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