Compare commits

..

No commits in common. "51de013007a7daefe86f21aa66428e20cbec11ef" and "ef993aae149b1c636cf6bbdd6efc4ea14d93ff2f" have entirely different histories.

1310 changed files with 21846 additions and 58267 deletions

View file

@ -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; \

View file

@ -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

View file

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

View file

@ -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: {

View file

@ -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
View file

@ -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
View file

@ -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

View file

@ -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>

View file

@ -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 `&nbsp;` 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

View file

@ -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 | |

View file

@ -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.

View file

@ -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);

View file

@ -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
]);
});

View file

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

View file

@ -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];
}

View file

@ -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);

View file

@ -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
"'": '&#x27;'
},
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
};

View file

@ -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')),

View file

@ -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();

View file

@ -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) =>

View file

@ -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));

View file

@ -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;
}

View file

@ -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)

View file

@ -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});
});
*/
}

View file

@ -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
View file

@ -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'] });
};

View 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>

View file

@ -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 => {

View file

@ -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);

View file

@ -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;

View file

@ -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?
}

View file

@ -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()) {

View file

@ -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)

View file

@ -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('@'))
}
}

View file

@ -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)
});
}

View file

@ -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})` : '');
}

View file

@ -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);
}
});
}
}
}

View file

@ -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;
});
}
}

View file

@ -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() {

View file

@ -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) : ''
));
}

View file

@ -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];
}
};

View file

@ -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)
);
}

View file

@ -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);

View file

@ -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 => {

View file

@ -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');
}

View file

@ -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)
);
}

View file

@ -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);

View file

@ -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() : ""))
});
}

View file

@ -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()
})
}
}

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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'));
}
});
}

View file

@ -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 => {

View file

@ -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
+ ';';

View file

@ -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
{
}

View file

@ -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';
}
}
/**

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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;
}
});
}

View file

@ -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;
}

View file

@ -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;
}
});
}

View file

@ -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;
}
});
}

View file

@ -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];
}
}
}

View file

@ -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'; }
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);
});

View file

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

View file

@ -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;
}
});
}

View file

@ -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 : ';'
);
}
}

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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));

View file

@ -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);

View file

@ -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))

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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) {

View file

@ -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;
});

View file

@ -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);
});
}

View file

@ -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);

View file

@ -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
}

View file

@ -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)
));

View file

@ -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());
}
};

View file

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

View file

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

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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: '☆'; /*⚐*/

View file

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

View file

@ -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 {

View file

@ -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] {

View file

@ -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();

View file

@ -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'));

View file

@ -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;

View file

@ -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